polymorphic controller: nested routes + polymorphic :has_many
Posted by michael.schaerfer on 26-Nov-08 at 15:02
If you have a polymorphic relationship between models and still want to use nested resources, there is always a problem with accessing the 'parent' context in your nested controller.
Imagine routes like:
1 map.resources :posts, :has_many => :comments 2 map.resources :articles, :has_many => :comments
and you get urls like "/posts/:postsid/comments" and "/articles/:articlesid/comments". The problem now is to determine the comment-context in your comments_controller.rb. If you only have two "parent" controller an "if-else" statement is a simple solution:
1 class CommentsController < ApplicationController 2 def index 3 if params[:articles_id] 4 @context = Article.find( params[:articles_id] 5 else 6 @context = Post.find( params[:post_id] 7 end 8 @comments = @context.comments 9 #... 10 end 11 end
But what if you have more polymorphic models and don't want to write an else-clause for every one of it? One solution is to define some "parent resources" in the comments_controller, as described here: http://revolutiononrails.blogspot.com/2007/05/drying-up-polymorphic-controllers.html.
Another solution is to use the routes definition directly, using the (not so well documented) :requirements option:
1 map.resources :articles, :has_many => :comments, :requirements => { :context_type => 'articles' }
unfortunately this definition just creates the :context_type parameter for the :articles resource and not for the nested one:
1 # Routes Example: 2 /articles/:id { :controller => 'articles', :action => 'show', :context_type => 'articles' } 3 /articles/:id/comments { :controller => 'comments', ;action => 'show' }
We have to be more explicit about the nested resource:
1 map.resources :articles do |articles| 2 articles.resources, :requirements => { :context_type => 'articles' 3 end
This actually does the same as the :has_many option, but now we can define the required context type parameter directly on the nested resource.
And now, in your controller, you can access the context model dynamically:
1 class CommentsController < ApplicationController 2 def index 3 @comments = context_object( :include => :comments ).comments 4 #... 5 end 6 7 private 8 9 def context_object( *finder_options ) 10 params[:context_type].classify.constantize.find( context_id, *finder_options ) 11 end 12 13 def context_id 14 params["#{ params[:context_type] }_id"] 15 end 16 17 end
the downside: for every polymorphic resource you have to define the :requirements option in your routes.
But i guess there is no way around this, except parsing the params hash for an "*_id" key, constantize the result and hoping nothing goes wrong and everyone respects the naming convention.
Namespaced Controller - get all sub-controller
Posted by michael.schaerfer on 19-Nov-08 at 16:52
Let's say we have a pretty big rails application with lots of namespaced controller like:
1 class AdminController < ApplicationController ; end 2 class Admin::UsersController < AdminController ; end 3 class Admin::PostsController < AdminController ; end
...and so on.
Now we want to build a method to get all subcontroller of a given controller, so that we can say:
1 AdminController.subcontroller 2 # => [Admin::UsersController, Admin::PostsController]
For every namespaced controller, rails builts a module with the controller-name (without "Controller") for the sub-controller.
So, we add a class method to AdminController:
1 class << self ; 2 def subcontroller 3 self.to_s =~/^(.+)Controller$/ 4 return [] unless $1 5 mod = $1 6 smod = $1.demodulize 7 if( pmod = self.parent ).const_defined?(:"#{smod}") 8 return ( mod = mod.constantize ).constants.reject { |r| 9 !( mod.const_get( r ) < ActionController::Base ) 10 } 11 end 12 return [] 13 end 14 end
Looks complicated, so lets get through this:
First we get the module name of the current controller( "AdminController" => "Admin" ).
Then we check if the constant ("Admin") is defined and, if it is, we return all constants/classes of this module that inherit from ActionController::Base, thus being a controller class.
If the module is not defined, we return an empty array, no subcontroller.
(All this "const_get(..)" stuff is used for more nested controllers, like "Admin::System::Config::PostsController".)
Now all the ruby-geeks out there are screaming "way too complicated! why not use this:"
1 Object.subclasses_of( AdminController ) 2 # => [Admin::UsersController, Admin::PostsController]
..and this works too. But keep in mind that the Object#subclasses_of method cycles through the whole ObjectSpace, trying to find an inherited class.
A little benchmark test on script/console:
1 >> n = 5000 2 => 5000 3 >> Benchmark.bm do |x| 4 ?> x.report { n.times do ; AdminController.subcontroller ; end } 5 >> x.report { n.times do ; Object.subclasses_of( AdminController ) ; end } 6 >> end 7 user system total real 8 0.310000 0.000000 0.310000 ( 0.312271) 9 61.190000 0.280000 61.470000 ( 62.760276) 10 => true
.. so the subcontroller method, using Module#constants, is a little bit faster..
register ActionMailer methods into Database
Posted by michael.schaerfer on 12-Nov-08 at 17:42
In the last posting, we talked about an ActionMailer active-check. For this approach to work, we need to register all instance-methods of all ActionMailer classes of our rails-app into the database.
The commonly used solution for this task is to search through your app/models directory, require all the *_mailer.rb classes and save them to the database. Like this:
1 Dir.glob("#{RAILS_ROOT}/app/models/*_mailer.rb").each do |file| 2 klass = File.basename( file, '.rb').classify.constantize 3 klass.instance_methods(false).each do |meth| 4 SystemMailer.find_or_create_by_mailer_class_and_mailer_method( 5 :mailer_class => klass.to_s, 6 :mailer_method => meth, 7 :active => true 8 ) 9 end 10 end
Put this into an initializer or your environment.rb file and you'r good to go. The downfall of this solution: You have to know all the directories of your Mailer classes, which might not be a problem, if you're following the "convention-over-configuration" rule inside your rails-app.
Another approach might be to get all subclasses of ActionMailer::Base:
1 ActionMailer::Base.subclasses_of( ActionMailer::Base ).each do |klass| 2 klass.instance_methods(false).each do |meth| 3 SystemMailer.find_or_create_by_mailer_class_and_mailer_method( 4 :mailer_class => klass.to_s, 5 :mailer_method => meth, 6 :active => true 7 ) 8 end 9 end
But this only works, if the subclasses of ActionMailer::Base are fully loaded/required (which is not the case, if your server just starts up).
check active ActionMailer
Posted by michael.schaerfer on 10-Nov-08 at 19:01
In the openSteam backend, we wanted to give the admins the possibility to manually activate or deactivate mailer, like order-confirmation or user-signup mails.
We built a model, called SystemMailer, which holds the mailer classname, the mailer method and a boolean.
Migration File:
1 class CreateSystemMailer < ActiveRecord::Migration 2 def self.up 3 create_table :system_mailers do |t| 4 t.string :mailer_method 5 t.string :mailer_class 6 t.boolean :active 7 8 t.timestamps 9 end 10 end 11 12 def self.down 13 drop_table :system_mailers 14 end 15 end
Model:
1 class SystemMailer < ActiveRecord::Base 2 def active? ; self.active ; end# 3 named_scope :active, { :conditions => { :active => true } } 4 end
Then we built an alias method chain for the ActionMailer::Base#deliver! method, to check whether the current mail-method of the current mailer is active:
1 class ActionMailer::Base 2 3 def deliver_with_active_mailer_check!(mail) 4 active_mailer = SystemMailer.find( :all, 5 :conditions => { 6 :mailer_class => self.class.to_s, 7 :mailer_method => @template, 8 :active => true } ) 9 10 return nil if active_mailer.empty? 11 12 deliver_without_active_mailer_check!( mail ) 13 end 14 15 alias_method_chain :deliver!, :active_mailer_check 16 17 end
In this alias-method, we try to fetch an active SystemMailer entry (with the current class and method name). If the results are empty (no active SystemMailer found), we return nil, otherwise we call the original deliver! method. Pretty simple.
Accessible Rich Internet Applications @ access-see-be.net
Posted by michael.schaerfer on 15-Oct-08 at 14:07
Alexander Gewessler, a co-developer and friend @ diamonddogs, has released a javascript library for "accessible rich internet application", based on the WAI-ARIA working draft from the W3C.
From his website:
Embedding Accessibility in Rich Internet Applications is of great importance for achieving the goal of Universal Design, the approach to design products that can be used by all people without the need for adaptation or specialization.
Website: www.access-see-be.net
The source is also available at github.
