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 options = joins.extract_options! if joins.empty? joins = ActsAsJoinable.models relationships = [:child] else relationships = [options[:as] || :parent].flatten.map(&:to_sym) end options[:class_name] ||= options[:source].to_s.camelize if options[:source] association_type = options[:limit] == 1 ? :has_one : :has_many scope_name = options[: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 = options[:class_name] || nil # contexts defining the relationship between self and target contexts = [options[:context] || []].flatten context = contexts.first # possible values of the context values = [options[:values] || options[:value] || []].flatten.compact value = values.first sql = options[:conditions] nestable = options[:nestable] || false # parent, child, or contexts (both) for custom helper getters/setters has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :dependent => :destroy, :foreign_key => "child_id", :uniq => true has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :dependent => :destroy, :foreign_key => "parent_id", :uniq => true 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 else plural_type = type.to_s end class_name = options[: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 } has_both_relationships = relationships.length > 1 # don't know how to do this with has_many, since the join model requires 2 # different polymorphic models, and it breaks if they're the same class if has_both_relationships # you can only get all objects by this, method. # to create them, use `child_(posts)`, `parent_(posts)`, etc. # define_method type do # [:parent, :child].map do |relationship| # send("#{relationship.to_s}_#{type.to_s}") if respond_to?("#{relationship.to_s}_#{type.to_s}") # end.flatten.compact.uniq # end end # relationships == [:parent, :child] relationships.each do |relationship| relationship = opposite_for(relationship) singular_relationship = "#{relationship.to_s}_relationship".to_sym plural_relationship = "#{relationship.to_s}_relationships".to_sym plural_relationship = "#{relationship.to_s}_relationships".to_sym if association_type == :has_one relationship_with_context = "#{relationship.to_s}_#{singular_type}_relationship".to_sym else relationship_with_context = "#{relationship.to_s}_#{singular_type}_relationships".to_sym end options = options.merge( :through => relationship_with_context, :source => relationship ) join_value = value if join_context unless join_context == class_name.underscore condition_string = "(#{ActsAsJoinable::Relationship.table_name.to_s}.context IN (?))" unless join_value # join_value = singular_type.to_s end condition_string << " AND (#{ActsAsJoinable::Relationship.table_name.to_s}.value = ?)" if join_value join_contexts = [join_context, class_name.underscore] 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, :dependent => :destroy } through_options[:uniq] = true unless association_type == :has_one # has_many :member_relationships # conditions: context == x, value == y send(:has_many, relationship_with_context, through_options) options = options.merge( :through => relationship_with_context, :source => relationship, :uniq => true ) end options.delete(:after_add) if association_type == :has_one options.delete(:uniq) if association_type == :has_one method_scope = association_type == :has_one ? :protected : :public send(method_scope) # has_many :child_users, :through => :child_relationships add_association(relationship.to_s, plural_type, options, join_context, join_value, &block) accepts_nested_attributes_for plural_type if nestable if association_type == :has_one define_method singular_type do send(plural_type).first end define_method "#{singular_type}=" do |value| send(relationship_with_context).destroy_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 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 add_association(relationship, plural_type, options, join_context, join_value, &block) eval_options = {:context => join_context} eval_options[:value] = join_value unless join_value.blank? send(:has_many, "#{relationship}_#{plural_type}".to_sym, options) do class_eval <<-EOF def construct_join_attributes(associate) super.merge(#{eval_options.inspect}) end EOF end # has_many :users, :through => :child_relationships send(:has_many, plural_type, options) do class_eval <<-EOF def construct_join_attributes(associate) super.merge(#{eval_options.inspect}) end EOF end end end module InstanceMethods end end end