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