module DattsRight module Base def has_dynamic_attributes(options={}) include DattsRight::InstanceMethods cattr_accessor :dynamic_attributes_options self.dynamic_attributes_options = options has_many :dynamic_attributes, :as => :attributable, :dependent => :destroy has_one :dynamic_attribute_definition, :as => :attribute_defineable, :dependent => :destroy after_save :save_dynamic_attribute_definition after_create :create_dynamic_attribute_definition_if_needed, :inherit_definition delegate :definition, :definition=, :to => :dynamic_attribute_definition # Carry out delayed actions before save before_save :build_dynamic_attributes after_find :cache_dynamic_attributes default_scope includes(:dynamic_attributes).includes(:dynamic_attribute_definition) # scope :scope_self when looking through attributes so we don't look through all dynamic_attributes # Why? What if you have Friend and Page models. # * Some Phone records have a dynamic_attribute :price # * Some Page records have a dynamic_attribute :price # # When we do Page.find_by_price(400) we want to search only the dynamic_attributes that belong to Page # and we want to disregard the rest of the dynamic_attributes. scope :scope_self, lambda { joins(:dynamic_attributes).where("dynamic_attributes.attributable_type = :klass", :klass => self.name) } scope :with_datt_key, lambda { |args| with_dynamic_attribute_key(args) } scope :with_dynamic_attribute_key, lambda { |datt_key| scope_self.joins(:dynamic_attributes).where("dynamic_attributes.attr_key = :datt_key", :datt_key => datt_key)} scope :with_datt_type, lambda { |args| with_dynamic_attribute_type(args) } scope :with_dynamic_attribute_type, lambda { |object_type| scope_self.joins(:dynamic_attributes).where("object_type = :object_type", :object_type => object_type) } scope :order_by_datt, lambda { |attr_key_with_order, object_type| order_by_dynamic_attribute(attr_key_with_order, object_type) } scope :order_by_dynamic_attribute, 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 = "dynamic_attributes.#{object_type}_value" order_value << " #{order_by}" if order_by scope_self.with_dynamic_attribute_key(attr_key).joins(:dynamic_attributes).with_dynamic_attribute_type(object_type).order(order_value) } scope :where_datt, lambda { |opts| where_dynamic_attribute(opts) } scope :where_dynamic_attribute, 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 attributes.each do |k, v| conditions = "exists (" + "select 1 from dynamic_attributes dynamic_attribute where " + "#{self.table_name}.id = dynamic_attribute.attributable_id " + "and dynamic_attribute.attributable_type = :attributable_type " + "and dynamic_attribute.attr_key = :attr_key and dynamic_attribute.#{DynamicAttribute.attr_column(v)} = :value" + ")" results = results.where(conditions, :attributable_type => self.name, :attr_key => k.to_s, :value => v) end results } # Used when you have already existing records that have don't have definitions, and you want them to def self.create_definitions! self.all.each do |record| record.create_dynamic_attribute_definition_if_needed end end private def self.method_missing(method_id, *arguments) # TODO better way to hook this into the rails code, and not define my own begin # Prioritize ActiveRecord's method_missing super(method_id, *arguments) rescue NoMethodError => e if method_id.to_s =~ /^find_(all_|last_)?by_(dynamic_attribute|datt)_([_a-z]\w*)$/ all_or_last = $1 attributes = $3.split("_and_") results = self attributes.each_with_index do |attribute, i| results = results.where_dynamic_attribute(attribute.to_sym => arguments[i]) end case all_or_last when "all_" results when "last_" results.last when nil results.first else nil end else raise e end 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 alias :has_datts :has_dynamic_attributes end end