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 2.3 + engines + helpers

Posted by michael.schaerfer on 28-Mar-09 at 20:50

According to this ticket #1905, app/helpers in rails plugins-engines doesn't get mixed into the ActionView::Base, and so the methods are not available in the views.

The reason for this is, that the "all_application_helpers" method in ActionController::Helper only returns the helper modules in RAILS_ROOT/app/helper and not those in vendor/plugins/**/app/helpers:

   1  #file: action_controller/helpers.rb
   2  #line: 219
   3          # Extract helper names from files in app/helpers/**/*.rb
   4    def all_application_helpers
   5      extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
   6      Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
   7    end
   8  

So when you say

   1  helper :all
in your controller, the helper-methods of your plugin are not available.

BUT, ... all the helper modules of your plugin are added to the load_path, so a quick fix for this problem would be to load the helper-module manually

   1  # for every helper file in your engines:
   2  helper :my_plugin_helper

Then the corresponding module-name is "constantized" and included into ActionView::Base by the "add_template_helper" method:

   1  #file: action_controller/helpers.rb
   2  #line: 75
   3    def add_template_helper(helper_module) #:nodoc:
   4      master_helper_module.module_eval { include helper_module }
   5    end

..and all helper-methods of your plugin/engine are available in your views.

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..