Nested namespaces in RESTful Rails

November 29th, 2007

Using RESTful Rails definitely has it advantages. The clean code was the primary reason why I switched all my projects toward REST. But I found that the nesting of resources broke down when using modules. I’d like to keep my code base nice and logically separated. For example the admin controller should reside in the app/controllers/admin/ directory. This will make your code base clearer and thus easier to maintain. And beside that it will generate pretty URL’s in which it is instantly clear where one is.

In the current project I integrated Beast and thought it was a good idea to keep this in a separate module as well. But the application I was working on required that forums only had relevance in the context of a project(there are many many different projects). So the obvious thing to do is to create a namespace nested in the projects resource.


map.resources :projects do |projects|
projects.resources :members
projects.namespace(:wiki) do |wiki_namespace|
wiki_namespace.resources :pages, :member => {:auto_save => :put}
end
end

And of course I wouldn’t be writing this post if this would work. Somehow the routes generated completely omitted the ‘wiki’ part:


new_project_page GET    /projects/:project_id/pages/new                                                        {:action=>"new", :controller=>"pages"}
formatted_new_project_page GET    /projects/:project_id/pages/new.:format                                                {:action=>"new", :controller=>"pages"}
edit_project_page GET    /projects/:project_id/pages/:id/edit                                                   {:action=>"edit", :controller=>"pages"}
formatted_edit_project_page GET    /projects/:project_id/pages/:id/edit.:format                                           {:action=>"edit", :controller=>"pages"}
auto_save_project_page PUT    /projects/:project_id/pages/:id/auto_save                                              {:action=>"auto_save", :controller=>"pages"}
formatted_auto_save_project_page PUT    /projects/:project_id/pages/:id/auto_save.:format                                      {:action=>"auto_save", :controller=>"pages"}
project_page GET    /projects/:project_id/pages/:id                                                        {:action=>"show", :controller=>"pages"}

Weird. Not to mention inconvenient. Eventually I got it to work:


map.resources :projects do |projects|
projects.resources :members
projects.resources :pages, :controller => "Wiki::Pages", :path_prefix => "/projects/:project_id/wiki", :name_prefix => "project_wiki_", :member => {:auto_save => :put}
end

The :path_prefix parameter appears to overwrite the path_prefix defined by the block defining the scope. I would have preferred to write :path_prefix => "wiki". As one can see this solution is very unbecoming. And my RSpec tests started failing… With RoutingErrors. Rails worked just fine but RSpec did not play nice with this hack(because that is what this is, a hack). The RSpec mailinglist nor the Rails mailinglist could help.

This led me to abandon testing. Yes, I know, bad idea. Which I soon found out. This time I dove in the source code and found out that the namespace method takes an argument called namespace. Fiddling around with this parameter I got to the following solution:


map.resources :projects do |projects|
projects.resources :members
projects.namespace(:wiki, :namespace => '') do |wiki_namespace|
wiki_namespace.resources :pages, :member => {:auto_save => :put}
end
end

This generated the correct routes and satisfied RSpec! Don’t ask me for the rational behind this obviously strange behavior.

attachment_fu dances with Capistrano

November 13th, 2007

It is one of these joyous revelations. Today I was having a discussion with a fellow developer on attachment_fu. I am very much a fan of plugins, this means less work for me. In which case I win. He, however, refrained from using attachment_fu. Instead writing his own file upload mechanism. This seemed strange to me and I asked him why. He then correctly pointed out that with attachment_fu he was unable to save files outside his application root directory.

So why is this a problem? Everyone I know is using Capistrano as their preferred way of deploying. The way Capistrano does subsequent deploys is by linking the new release to the current directory and unlinking the old one. Everything in the old release suddenly becomes unavailible! And that includes the files by attachment_fu. And article on Almost Effortless pointed this out several months ago and posted a solution in the form of a Capistrano recipe. But who wants to write their own recipes?

However I seemed to remember seeing a @system@ directory on our live server. And there is! In Capistrano 2.0 every public directory has a link to a system directory which is unaffected by deploys. How well these Capistrano people thought this out. Brilliant. Now it is only a matter of using the :path_prefix argument.


has_attachment :content_type => :image,
:storage => :file_system,
:path_prefix => 'public/system',
:max_size => 500.kilobytes,
:resize_to => '320x200>',
:thumbnails => { :thumb => '100x100>' }

Set svn propset svn:ignore "*" public/system/ and you are ready to go!

Acts_as_versioned quirks

November 11th, 2007

The Rails plugin acts_as_versioned by Rick Olson is a blessing when one wants to do some simple versioning. Building a wiki springs into mind which was exactly what I wanted to use it for. Following the learn_to post I was able to get most of it working. However I my case, and I imagine others, I did not want to version control my entire pages table. I do not care about the @created_at@ and @updated_at@ fields to be versioned. But using the default migration tactic of building a versioned table did not work (passing a block to the Page.create_versioned_table). Instead I needed to configure my model with:

self.non_versioned_columns << 'project_id' << 'project_user_id' << 'locked_by' << 'locked_at' << 'created_at' << 'updated_at'

Not something I would expect.

And it turned out there were more unexpected ‘features’. The inclusion of the timestamps in my Page model seemed superfluous. The generated @page_versions@ table included them by default. But there is no easy way for dropping these columns from the @pages@ table. Sure I could write a migration doing just that but by then the generated @page_versions@ table already contained these columns and apparently the generated class Page::Version referenced to these column. This obviously led to errors. Instead I ended up dropping and recreating the entire table. Unfortunately MySQL died complaining that the column @version@ already exists in the @pages@ table. You’ll have to remove that column too.

Finding a previous version has changed somewhat too. According to Rails Recipes;


>> p = Page.find(:first)
>> p = p.find_version(3)
NoMethodError: undefined method `find_version' for #<Page:0x329a5d8>

This should work but it does not. Actually I would love for it to work like that. The alternative is;


>> p = Page.find(:first)
>> array = Page.find_versions(:first)
>> p = p.revert_to(arr[2])

Not as pretty but it works.

On the bright side there is this neat feature called @:if_changed@. This allows you to control when a new version should be created. The directive is given as an argument in the acts_as_versioned declaration:

acts_as_versioned :if_changed => [:title, :text, :member_id]

Sweet now a new version is created only when the title, the text or the owner changes.

In conclusion, the acts_as_versioned plugin is a great extension but seems a bit outdated as not all functionality works(at least not in Rails 2.0RC1). Also it is better to create the documentation from the source as all the online documentation is very old.