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

has_many + dynamic conditions

Posted by michael.schaerfer on 06-Apr-09 at 16:36

In our latest project we had to implement condition-based categories. (like a category which holds all products with a price greater than X).

We serialized the condition-hash into a 'category_conditions' column and implemented a proxy-association method to evaluate the condition and return a scope (to keep method-chaining working):

   1  class Category < ActiveRecord::Base
   2    has_many :products do
   3      def with_condition
   4        scoped( { :conditions => proxy_owner.category_conditions } )
   5      end
   6    end
   7  end

No we can use it like this:

   1  c = Category.create( :category_condition => ["price >= ?", 99] )
   2  c.products # => regular associated products
   3  c.products.with_condition # => products with a price greater than 99

Rails + MS SQL on Mac OS X

Posted by michael.schaerfer on 28-Jan-09 at 18:53

In a recent opensteam project, we had to work with a legacy database on a MS SQL Server 2005.
Well, connecting rails to a mssql server can be a pain. But after some research we made it work on a windows-client (using the ADO driver) and on a linux (ubuntu) client (using unixODBC and freetds), by following the instructions at http://piao-tech.blogspot.com/2008/02/using-activerecord-with-microsoft-sql.htm

Yesterday i wanted to test the connection on my mac and installed unixodbc and freetds via MacPorts, but i always got a "Unexpected EOF from the server" error, when testing the connection-settings with tsql.


FreeTDS

Turns out, the solution is to install a freetds variant via MacPorts:

   1  $> sudo port install freetds +msql

which configures freetds with "—with-tdserv=8.0 —enable-msdblib".

And now the connection with tsql works:

   1  $> tsql -H hostname -U username -p port
   2  locale is "de_AT.UTF-8"
   3  locale charset is "UTF-8"
   4  Password:
   5  1> # we're in!!



iODBC

Now we have to configure ODBC with FreeTDS. Since OSX 10.5 comes pre-installed with iodbc, we make our life simple and use it.

Edit /Library/ODBC/odbcinst.ini: (mine looks like this)

   1  [ODBC Drivers]
   2  tds = Installed
   3  
   4  [tds]
   5  Driver = /opt/local/lib/libtdsodbc.so
   6  Setup  = /opt/local/lib/libtdsodbc.so

and /Library/ODBC/odbc.ini

   1  [ODBC]
   2  Trace = 0
   3  
   4  [A_DSN]
   5  Driver = TDS
   6  Description = ODBC connection via FreeTDS
   7  Trace = No
   8  Server = hostname_or_ip 
   9  Database = DATABASE_NAME
  10  Port = 1410

Here we're using a "freetds.conf"-less configuration and specify the server and port directly in the odbc.ini file. For more configuration settings see http://cubist.cs.washington.edu/doc/FreeTDS/userguide/x1853.htm.

Now we can test our settings with iodbctest

   1  $> iodbctest "dsn=A_DSN;uid=USERNAME;pwd=PASSWORD"
   2  iODBC Demonstration program
   3  This program shows an interactive SQL processor
   4  Driver Manager: 03.52.0406.1211
   5  Driver: 0.82 (libtdsodbc.so)
   6  
   7  SQL> # <= In Again!!

Rails + ActiveRecord

First install some gems:

   1  sudo gem install dbi dbd-odbc
   2    sudo gem install rails-sqlserver-2000-2005-adapter -s http://gems.github.com
   3    # or
   4    sudo gem install activerecord-sqlserver-adapter --source=http://gems.rubyonrails.org

Second, install the ruby-odbc bindings from http://www.ch-werner.de/rubyodbc/

   1  $> tar xvzf ruby-odbc-0.9995.tar.gz
   2  $> cd ruby-odbc-0.9995
   3  $> ruby extconf.rb
   4  $> make
   5  $> sudo make install

Third, fire up irb and test if everything is working:

   1  >> require 'dbi'
   2  => true
   3  >> DBI.connect('dbi:ODBC:A_DSN', 'USERNAME', 'PWD' )
   4  => #<DBI::DatabaseHandle:0x1200318...>

And now with ActiveRecord:

   1  >> require 'activerecord'
   2  => true
   3  >> h = { :dsn => 'A_DSN', :password => 'pwd', :username => 'username', :mode => 'odbc', :adapter => 'sqlserver' }
   4  >> ActiveRecord::Base.establish_connection(h)
   5  => #<ActiveRecord::ConnectionAdapters::ConnectionPool:0x183ad50 ..>
   6  
   7  >> ActiveRecord::Base.connection.tables
   8  => ["...", "..."] # should display your tables

And we have successfully connected Rails to a MS SQL Server!

Keep in mind to allow tcp/ip connections on your MS SQL Server (Configuration Tools) and allow remote access for your user on the server (Management Tools).

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.