require 'datts_right/instance_methods' require 'datts_right/datt' #require 'datts_right/query_methods' #require 'datts_right/exceptions' module DattsRight def has_datts(options={}) include DattsRight::InstanceMethods #include DattsRight::QueryMethods #ActiveRecord::QueryMethods.extend DattsRight::QueryMethods #include DattsRight::Exceptions cattr_accessor :datts_options self.datts_options = options has_many :datts, :as => :attributable, :dependent => :destroy # Carry out delayed actions before save before_save :build_dynamic_attributes # scope :scope_self when looking through attributes so we don't look through all datts # Why? What if you have Friend and Page models. # * Some Phone records have a datt :price # * Some Page records have a datt :price # # When we do Page.find_by_price(400) we want to search only the datts that belong to Page # and we want to disregard the rest of the datts. scope :scope_self, lambda { joins(:datts).where("datts.attributable_type = :klass", :klass => self.name) } scope :dattr_key, lambda { |attr_key| scope_self.joins(:datts).where("datts.attr_key = :attr_key", :attr_key => attr_key)} scope :dattr_type, lambda { |object_type| scope_self.joins(:datts).where("object_type = :object_type", :object_type => object_type) } scope :order_datt, lambda { |attr_key_with_order, object_type| # possible attr_key_with_order forms: "field_name", "field_name ASC", "field_name DESC" split_attr_key_with_order = attr_key_with_order.split(" ") attr_key = split_attr_key_with_order.first order_by = split_attr_key_with_order.last if split_attr_key_with_order.size > 1 order_value = "datts.#{object_type}_value" order_value << " #{order_by}" if order_by scope_self.dattr_key(attr_key).joins(:datts).dattr_type(object_type).order(order_value) } scope :where_datt, lambda { |opts| # TODO accept stuff other than the normal hash # Lifted from AR::Relation#build_where attributes = case opts when String, Array self.expand_hash_conditions_for_aggregates(opts) when Hash opts end results = self#joins(:datts) # Look for all DattsRight records whose: # 1. Has all dynamic_attributes # 2. Those dynamic attributes match there conditions attributes.each do |k, v| conditions = "exists (" + "select 1 from datts datt where " + # restricting the attributable_id to pages.id forces all the exists clauses to reference a single attributable record. Layman's terms, assuming Page uses datts_right: # find only pages that has the matching dynamic attributes "#{self.table_name}.id = datt.attributable_id " + "and datt.attributable_type = :attributable_type " + "and datt.name = :name and datt.#{Datt.attr_column(v)} = :value" + ")" results = results.where(conditions, :attributable_type => self.name, :name => k.to_s, :value => v) end results } def self.method_missing(method_id, *arguments) # TODO better way to hook this into the rails code, and not define my own if method_id.to_s =~ /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ attributes = $2.split("_and_") #puts "Will find by #{attributes.inspect}" arg = arguments.first scope_self.joins(:datts).where("datts.#{Datt.attr_column(arg)} = :value", :value => arg) end end # Override AR::Base#respond_to? so we can return the matchers even if the # attribute doesn't exist in the actual columns. Is this expensive? def respond_to?(method_id, include_private=false) # TODO perhaps we could save a cache somewhere of all methods used by # any of the records of this class. that way, we can make this method # act a bit more like AR::Base#respond_to? # Ex: # return true if all_attributes_exists?(match.attribute_names) || all_dynamic_attributes_exists?(match.attribute_names) if match = ActiveRecord::DynamicFinderMatch.match(method_id) return true elsif match = ActiveRecord::DynamicScopeMatch.match(method_id) return true end super end end end ActiveRecord::Base.extend DattsRight