module ActsAsJoinable module Core def self.included(base) base.send :include, ActsAsJoinable::Core::InstanceMethods base.extend ActsAsJoinable::Core::ClassMethods base.initialize_acts_as_joinable_on_core end module ClassMethods # custom parent, self, and child contexts # Group... # acts_as_joinable_on :groups, # :as => [:parent, :child], # :context => :nested_groups, # :child_classes => %w(pod store) # acts_as_joinable_on :pods, :as => :parent, :class_name => "Group" # acts_as_joinable_on :users, # :as => :parent, # :context => :membership # :values => %w(owner developer admin consumer) # acts_as_joinable_on class|context|custom_alias, class, role, context, context values # acts_as_joinable_on :members, :class_name => "User", :as => :parent, :context => :memebership # acts_as_joinable_on :owner, :class_name => "User", :context => :memebership, :value => :owner # joins_one :owner, :class_name => "User", :context => :memebership, :value => :owner # joins_many :members, :class_name => "User", :context => :memebership # joins :user, :with => :role do # has_many :board_of_directors # has_one :owner # end # has_many_parent :posts # has_many_child :assets # has_many_relationships :users, :through => :memberships # joins :user, :with => :membership do # has_many :members # end # Office # acts_as_joinable_on :pods, :as => :child # acts_as_joinable_on :tenants, :as => :parent # User def initialize_acts_as_joinable_on_core joins = acts_as_joinable_config.dup block = joins.pop opts = joins.extract_options! if joins.empty? joins = ActsAsJoinable.models relationships = [:child] else relationships = [opts[:as] || :parent].flatten.map(&:to_sym) end opts[:class_name] ||= opts[:source].to_s.camelize if opts[:source] association_type = opts[:limit] == 1 ? :has_one : :has_many scope_name = opts[:named_scope] joins.map!(&:to_sym) # class name of the model we're joining to self # otherwise it's retrieved from joins.each... class_name = opts[:class_name] || nil # contexts defining the relationship between self and target contexts = [opts[:context] || []].flatten context = contexts.first # possible values of the context values = [opts[:values] || opts[:value] || []].flatten.compact value = values.first sql = opts[:conditions] nestable = opts[:nestable] || false # parent, child, or contexts (both) for custom helper getters/setters has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :foreign_key => "child_id", :uniq => true has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :foreign_key => "parent_id", :uniq => true after_destroy :destroy_relationships unless after_destroy.map(&:method).include?(:destroy_relationships) related_classes = (ancestors.reverse - included_modules + send(:subclasses)).uniq wanted_classes = [] while wanted_classes.push(related_classes.pop) break if wanted_classes.last == base_class.superclass end wanted_classes.pop joins.each do |type| if association_type == :has_one singular_type = type.to_s.to_sym plural_type = type.to_s.pluralize.to_sym else singular_type = type.to_s.singularize.to_sym plural_type = type.to_s.to_sym end class_name = opts[:class_name] || type.to_s.classify join_context = (context || singular_type).to_s options = { :through => :relationships, :class_name => class_name, :source => :child, :source_type => class_name, :conditions => sql } # relationships == [:parent, :child] relationships.each do |relationship| # Post.joins :tags, :as => :parent # through_relationship = child_tag_relationships # relationship_table = `relationships` relationship = opposite_for(relationship) through_relationship = "#{relationship.to_s}_#{singular_type}_relationships".to_sym relationship_table = ActsAsJoinable::Relationship.quoted_table_name options.merge!(:through => through_relationship, :source => relationship, :uniq => true) join_value = value # conditions for the join model condition_string = "" #if join_context == class_name.underscore # condition_string << "(#{relationship_table}.#{relationship}_type IN (?))" if join_context == class_name.underscore #conditions = [condition_string, [class_name]] else condition_string << "(#{relationship_table}.context IN (?))" condition_string << " AND (#{relationship_table}.value = ?)" if join_value join_contexts = [join_context]#[join_context, class_name.underscore].uniq conditions = [condition_string, join_contexts] conditions << join_value.to_s if join_value end through_options = { :class_name => "ActsAsJoinable::Relationship", :conditions => conditions, :as => opposite_for(relationship).to_sym } if association_type == :has_one options.delete(:after_add) options.delete(:uniq) else through_options[:uniq] = true end unless has_association?(through_relationship) has_many through_relationship, through_options end add_association(relationship.to_s, plural_type, options, join_context, join_value, &block) if association_type == :has_one add_has_one(singular_type, plural_type, through_relationship, class_name, join_context, join_value, options) add_class_relationship_method(singular_type) else add_class_relationship_method(plural_type) end if nestable accepts_nested_attributes_for plural_type.to_sym if association_type == :has_one define_method "#{singular_type}_attributes=" do |params| params = params.first if params.is_a?(Array) self.send("#{plural_type}_attributes=", [params]) end end end end end end def acts_as_joinable_on(*args) super(*args) initialize_acts_as_joinable_on_core end def find_joined_with_join(model, conditions = {}) options = join_conditions(model, conditions.delete(:relationship) || {}) options.merge!(conditions) source_type.all(options) end def find_joined(model, conditions = {}) join_conditions(model, conditions.delete(:relationship) || {}) do |kind, source_type, relationship_conditions| ids = ActsAsJoinable::Relationship.select_attributes("#{opposite_for(kind)}_id", relationship_conditions).uniq source_type.all(:conditions => {:id => ids}.merge(conditions)) end end def find_from_joined(model, conditions = {}) action_from_joined(:all, model, conditions) end def count_from_joined(model, conditions) action_from_joined(:count, model, conditions) end def action_from_joined(action, model, conditions) join_conditions(model, conditions.delete(:relationship) || {}) do |kind, source_type, relationship_conditions| ids = ActsAsJoinable::Relationship.select_attributes("#{kind}_id", relationship_conditions).uniq send(action, :conditions => {:id => ids}.merge(conditions)) end end # def join_scope(name, conditions = {}, &block) #add_class_relationship_method(method) # end def join_conditions(model, conditions = {}, &block) relationship = self.reflect_on_all_associations.detect {|a| a.name == model.to_sym} if relationship options = relationship.options else options = {:source => "parent", :source_type => model.to_s.camelize} end kind = conditions[:as] || options[:source].to_s source_type = options[:source_type].constantize result = conditions.reverse_merge( "#{opposite_for(kind)}_type" => related_classes.map(&:name), "#{kind}_type" => source_type.related_classes.map(&:name) ) if block_given? yield(kind, source_type, result) else result end end private def opposite_for(role) role.to_s == "parent" ? "child" : "parent" end def add_class_relationship_method(method) class_eval <<-EOF def self.#{method}(options = {}) find_joined(:#{method.to_s}, options) end EOF end def has_association?(name) self.reflect_on_all_associations.map(&:name).include?(name.to_sym) end def add_has_one(singular_type, plural_type, relationship_with_context, class_name, join_context, join_value, options) define_method singular_type do send(plural_type).last end define_method "#{singular_type}=" do |value| #send(relationship_with_context).delete_all #send(relationship_with_context).delete_all unless value.blank? send(plural_type) << value end send(plural_type).each do |object| send(plural_type).delete(object) unless object.id == value.id end end define_method "#{singular_type}_id" do send(singular_type).id rescue nil end define_method "#{singular_type}_id=" do |id| item = id.blank? ? nil : class_name.constantize.find_by_id(id) send("#{singular_type}=", item) end end def add_association(relationship, plural_type, options, join_context, join_value, &block) eval_options = {} eval_options[:context] = join_context unless join_context.to_s == options[:class_name].to_s.underscore.singularize eval_options[:value] = join_value unless join_value.blank? # has_many :users, :through => :child_relationships plural_relationship = "#{relationship}_#{plural_type}".to_sym unless has_association?(plural_relationship) send(:has_many, plural_relationship, options) do class_eval <<-EOF def construct_join_attributes(associate) super.merge(#{eval_options.inspect}) end EOF end end unless has_association?(plural_type) send(:has_many, plural_type.to_sym, options) do class_eval <<-EOF def construct_join_attributes(associate) super.merge(#{eval_options.inspect}) end EOF end end end end module InstanceMethods def destroy_relationships conditions = %Q|(`relationships`.parent_type IN ("#{self.class.name}","#{self.class.base_class.name}") AND `relationships`.parent_id = #{self.id}) OR (`relationships`.child_type IN ("#{self.class.name}","#{self.class.base_class.name}") AND `relationships`.child_id = #{self.id})| ActsAsJoinable::Relationship.delete_all(conditions) end def relationship_matches?(relationship, conditions) return false if conditions[:parent_type] && !Array(conditions[:parent_type]).include?(relationship.parent_type) return false if conditions[:child_type] && !Array(conditions[:child_type]).include?(relationship.child_type) return false if conditions[:context] && !Array(conditions[:context]).include?(relationship.context) true end def parent_relationships_for(conditions = {}) parent_relationships.select do |relationship| relationship_matches?(relationship, conditions) end end def child_relationships_for(conditions = {}) child_relationships.select do |relationship| relationship_matches?(relationship, conditions) end end end end end