Opensteam_blog_logo4
opensteam.net | rss | search | archive
Results (escape to close):

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.