Module | ActiveRecord::Acts::EavModel::ClassMethods |
In: |
lib/acts_as_eav_model.rb
|
Will make the current class have eav behaviour.
class Post < ActiveRecord::Base has_eav_behavior end post = Post.find_by_title 'hello world' puts "My post intro is: #{post.intro}" post.teaser = 'An awesome introduction to the blog' post.save
The above example should work even though "intro" and "teaser" are not attributes on the Post model.
The following options are available on for has_eav_behavior to modify the behavior. Reasonable defaults are provided:
class User < ActiveRecord::Base has_eav_behavior :class_name => 'UserContactInfo' has_eav_behavior :class_name => 'Preferences' def eav_attributes(model) case model when UserContactInfo %w(email phone aim yahoo msn) when Preference %w(project_search project_order user_search user_order) else Array.new end end end marcus = User.find_by_login 'marcus' marcus.email = 'marcus@example.com' # Will save to UserContactInfo model marcus.project_order = 'name' # Will save to Preference marcus.save # Carries out save so now values are in database
Note the else clause in our case statement. Since an empty array is returned for all other models (perhaps added later) then we can be certain that only the above eav attributes are allowed.
If both a :fields option and eav_attributes method is defined the fields option take precidence. This allows you to easily define the field list inline for one model while implementing eav_attributes for another model and not having eav_attributes need to determine what model it is answering for. In both cases the list of flex attributes can be a list of string or symbols
A final alternative to :fields and eav_attributes is the is_eav_attribute? method. This method is given two arguments. The first is the attribute being retrieved/saved the second is the Model we are testing for. If you override this method then the eav_attributes method or the :fields option will have no affect. Use of this method is ideal when you want to retrict the attributes but do so in a algorithmic way. The following is an example:
class User < ActiveRecord::Base has_eav_behavior :class_name => 'UserContactInfo' has_eav_behavior :class_name => 'Preferences' def is_eav_attribute?(attr, model) case attr.to_s when /^contact_/ then true when /^preference_/ then true else false end end end marcus = User.find_by_login 'marcus' marcus.contact_phone = '021 654 9876' marcus.contact_email = 'marcus@example.com' marcus.preference_project_order = 'name' marcus.some_attribute = 'blah' # If some_attribute is not defined on # the model then method not found is thrown
# File lib/acts_as_eav_model.rb, line 200 200: def has_eav_behavior(options = {}) 201: 202: # Provide default options 203: options[:class_name] ||= self.class_name + 'Attribute' 204: options[:table_name] ||= options[:class_name].tableize 205: options[:relationship_name] ||= options[:class_name].tableize.to_sym 206: options[:foreign_key] ||= self.class_name.foreign_key 207: options[:base_foreign_key] ||= self.name.underscore.foreign_key 208: options[:name_field] ||= 'name' 209: options[:value_field] ||= 'value' 210: options[:fields].collect! {|f| f.to_s} unless options[:fields].nil? 211: options[:parent] = self.class_name 212: 213: # Init option storage if necessary 214: cattr_accessor :eav_options 215: self.eav_options ||= Hash.new 216: 217: # Return if already processed. 218: return if self.eav_options.keys.include? options[:class_name] 219: 220: # Attempt to load related class. If not create it 221: begin 222: options[:class_name].constantize 223: rescue 224: Object.const_set(options[:class_name], 225: Class.new(ActiveRecord::Base)).class_eval do 226: def self.reloadable? #:nodoc: 227: false 228: end 229: end 230: end 231: 232: # Store options 233: self.eav_options[options[:class_name]] = options 234: 235: # Only mix instance methods once 236: unless self.included_modules.include?(ActiveRecord::Acts::EavModel::InstanceMethods) 237: send :include, ActiveRecord::Acts::EavModel::InstanceMethods 238: end 239: 240: # Modify attribute class 241: attribute_class = options[:class_name].constantize 242: base_class = self.name.underscore.to_sym 243: 244: attribute_class.class_eval do 245: belongs_to base_class, :foreign_key => options[:base_foreign_key] 246: alias_method :base, base_class # For generic access 247: end 248: 249: # Modify main class 250: class_eval do 251: has_many options[:relationship_name], 252: :class_name => options[:class_name], 253: :table_name => options[:table_name], 254: :foreign_key => options[:foreign_key], 255: :dependent => :destroy 256: 257: # The following is only setup once 258: unless method_defined? :method_missing_without_eav_behavior 259: 260: # Carry out delayed actions before save 261: after_validation_on_update :save_modified_eav_attributes 262: 263: # Make attributes seem real 264: alias_method_chain :method_missing, :eav_behavior 265: 266: private 267: 268: alias_method_chain :read_attribute, :eav_behavior 269: alias_method_chain :write_attribute, :eav_behavior 270: 271: end 272: end 273: 274: create_attribute_table 275: 276: end