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| singular_type = type.to_s.singularize if association_type == :has_one plural_type = type.to_s.pluralize.to_sym else 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, nestable, &block) if association_type == :has_one add_has_one(singular_type, plural_type, through_relationship, class_name) end end end end def acts_as_joinable_on(*args) super(*args) initialize_acts_as_joinable_on_core end private def opposite_for(role) role.to_s == "parent" ? "child" : "parent" 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) define_method singular_type do send(plural_type).last end define_method "#{singular_type}=" do |value| send(relationship_with_context).delete_all send(plural_type) << value end define_method "#{singular_type}_id" do send(singular_type).id rescue nil end define_method "#{singular_type}_id=" do |id| send("#{singular_type}=", class_name.constantize.find(id)) end end def add_association(relationship, plural_type, options, join_context, join_value, nestable, &block) eval_options = {:context => join_context} 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 accepts_nested_attributes_for plural_type.to_sym if nestable 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 end end end