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..
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.
v0.9.3 released!
Posted by michael.schaerfer on 01-Oct-08 at 17:46
We finally released a new version of openSteam today.
This release introduces some major enhancements of the checkout- and order-process, like shipping-rate calculation, tax management and an ActiveMerchant integration for credit-card payments.
New Features:
- Taxes: define taxes or tax-groups for products and regions
- ActiveMerchant integration
- PaymentMethods: use implemented payment-methods or implement your own
- ShippingRate: define rates for products, regions and payment-types.
- PDF-Export for invoices (using the excellent prawnto plugin)
For a full list of changes please read the CHANGELOG, and if you're curious what to expect in future releases, please visit the Roadmap at our homepage.
In future posts we will try to describe some of the new features and their implementations in detail.
openSteam v0.9.2 released!
Posted by michael.schaerfer on 07-Aug-08 at 12:32
A new opensteam version is released to the world. This release fixes some bugs in the product-generators and admin-controller.
New Features:
- Apply filter to the sortable tables (admin-backend)
- SQLite3 compatibility
- RESTful controller (admin-backend)
And we want to thank Gregg and Jason from railsenvy.com for mentioning opensteam in their newest podcast.
openSteam v0.9.1 released!
Posted by michael.schaerfer on 01-Aug-08 at 09:02
We're pleased to announce the new openSteam release, v0.9.1!
The new version of the web-based shopping framework for RubyOnRails introduces many new features such as:
- Rails 2.1 compatible
- create Invoices and Shipments for an Order and even for Order-Items
- mark an Order, an Invoice or a Shipment as 'pending' or 'finished'. Or even create your own states and state-specific logic using modules.
- a whole new Admin-Backend (overview and process orders, manage admins and/or customers, etc)
- sortable and searchable tables in the admin-backend
- a simple search form, finally!
See the CHANGELOG for more information on what we've done.
You can easily install opensteam by typing gem install opensteam. Or, if you already have it installed, type gem update.
If you're a developer and/or you're not afraid to live on the edge, checkout the current developer version using svn:
svn checkout svn://rubyforge.org/var/svn/opensteam
If you have any question, drop us an email or visit our forum at rubyforge
form_tag block in a helper-method
Posted by michael.schaerfer on 24-Jul-08 at 07:45
According to this Ticket, using form_tag with a block in a helper-method is broken in rails 2.1.
Here is a, not so elegant but working, solution:
1 def render_form_tag_in_helper( _erbout, url, *args ) 2 form_tag( url ) do 3 concat( submit_tag("button"), binding ) 4 end 5 endWe have to pass the _erbout-variable to the helper-method in order to make the form_tag-method work. And to add elements to the form (submit_buttons, text_fields, etc), we have to call concat (to add the elements to the _erbout variable) with the current binding.
Column/Attribute names for a model
Posted by michael.schaerfer on 23-Jul-08 at 20:06
A quick way to get all column/attribute names for a model:
(normal columns are Strings, association-attributes are Symbols)
1 # returns all columns/attributes for a model 2 # column-names used for associations (foreign_keys) are replaces by their 3 # association-names (as symbol). 4 def get_column_names_for( record ) 5 # get all column-names, check for foreign_keys and replace them by association_name 6 record.columns.collect(&:name).collect { |c| 7 (a = check_association( record, c ) ).empty? ? c : a.collect(&:name) 8 }.flatten.uniq + 9 # get all has_one/has_many/habtm association names 10 record.reflect_on_all_associations.select { |a| a.macro.to_s =~/^has/ }.collect(&:name) 11 12 end 13 14 15 def check_association( record, string ) 16 # check if +string+ (like "artist_id") is a foreign_key for an association 17 # if yes, return association. 18 record.reflect_on_all_associations.select { |a| a.options[:foreign_key] ? 19 a.options[:foreign_key].to_sym == string.to_sym : 20 a.name.to_s.foreign_key.to_sym == string.to_sym } 21 end
Useful if you want to build like an automatic scaffold function or ...
State-Pattern using Modules
Posted by michael.schaerfer on 22-Jul-08 at 21:20
In the last weeks, we tried to implement a state-pattern for our orders, simply put: different states means different functionality.
We tried various approaches, like simple state-symbols, state-classes (to hold the state-specifc methods and return new states), state-associations and the very nice AASM plugin by Scott Barron.
But all this techniques felt very clumsy for our simple needs and not so ... 'ruby-like'.
Then i stumbled across Jay Fields Post and really liked the approach of using just state-modules and delegating to the instance_method ('cause all modules included in a class are just ancestors of this class!).
Here a brief overview of our solution:
1 # the order-class 2 class Order 3 include StateLogic 4 5 #states 6 include Finished 7 attr_accessor :state 8 endThe StateLogic Module (defines the 'fire_event' method to delegate to the state-module).
1 module StateLogic 2 def fire_event(name, *args, &block) 3 state_module = self.state.classify.constantize 4 5 if state_module.instance_methods(false).include?(name) 6 state_module.instance_method(m).bind( self ).call( *args, &block ) 7 else 8 puts "event '#{name}' not defined for state '#{state}'" 9 return false 10 end 11 end 12 endA State-Module
1 module Finished 2 def self.included(base) 3 self.instance_methods(false).each do |m| 4 base.class_eval do 5 define_method(m) { |*args| fire_event(m,*args) } 6 end 7 end 8 end 9 10 #an event 11 def say_something 12 puts "now in state '#{state}'" 13 end 14 15 endAs you can see, we override every state-module instance_method in the receiver-class to call the fire_event method. And in this method, we check if the current state-module defines such an instance_method, bind it to the order-instance and call it (or printing an error message, if no such method is defined in the current state-module.)
Pretty simple!
openSteam v0.9.1 soon!
Posted by michael.schaerfer on 22-Jul-08 at 21:08
We're currently working hard on the next openSteam release (v0.9.1).
A full list of new features will come in the next days, but for now: there will be invoices and shipments, lots of refactored code, many states and a simple extension-mechanism for the admin-backend.
