upgrade has_many_polymorphs

This commit is contained in:
Reinier Balt 2008-12-21 22:15:57 +01:00
parent 2d11109b8b
commit f97ca2f6af
178 changed files with 132 additions and 108 deletions

View file

@ -1,27 +0,0 @@
require 'active_record'
RAILS_DEFAULT_LOGGER = nil unless defined? RAILS_DEFAULT_LOGGER
require 'has_many_polymorphs/reflection'
require 'has_many_polymorphs/association'
require 'has_many_polymorphs/class_methods'
require 'has_many_polymorphs/support_methods'
require 'has_many_polymorphs/base'
class ActiveRecord::Base
extend ActiveRecord::Associations::PolymorphicClassMethods
end
if ENV['HMP_DEBUG'] or ENV['RAILS_ENV'] =~ /development|test/ and ENV['USER'] == 'eweaver'
require 'has_many_polymorphs/debugging_tools'
end
if defined? Rails and RAILS_ENV and RAILS_ROOT
_logger_warn "rails environment detected"
require 'has_many_polymorphs/configuration'
require 'has_many_polymorphs/autoload'
end
_logger_debug "loaded ok"

View file

@ -1,159 +0,0 @@
module ActiveRecord #:nodoc:
module Associations #:nodoc:
class PolymorphicError < ActiveRecordError #:nodoc:
end
class PolymorphicMethodNotSupportedError < ActiveRecordError #:nodoc:
end
# The association class for a <tt>has_many_polymorphs</tt> association.
class PolymorphicAssociation < HasManyThroughAssociation
# Push a record onto the association. Triggers a database load for a uniqueness check only if <tt>:skip_duplicates</tt> is <tt>true</tt>. Return value is undefined.
def <<(*records)
return if records.empty?
if @reflection.options[:skip_duplicates]
_logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue"
load_target
end
@reflection.klass.transaction do
flatten_deeper(records).each do |record|
if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record?
raise PolymorphicError, "You can't associate unsaved records."
end
next if @reflection.options[:skip_duplicates] and @target.include? record
@owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record))
@target << record if loaded?
end
end
self
end
alias :push :<<
alias :concat :<<
# Runs a <tt>find</tt> against the association contents, returning the matched records. All regular <tt>find</tt> options except <tt>:include</tt> are supported.
def find(*args)
opts = args._extract_options!
opts.delete :include
super(*(args + [opts]))
end
def construct_scope
_logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet."
super
end
# Deletes a record from the association. Return value is undefined.
def delete(*records)
records = flatten_deeper(records)
records.reject! {|record| @target.delete(record) if record.new_record?}
return if records.empty?
@reflection.klass.transaction do
records.each do |record|
joins = @reflection.through_reflection.name
@owner.send(joins).delete(@owner.send(joins).select do |join|
join.send(@reflection.options[:polymorphic_key]) == record.id and
join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}"
end)
@target.delete(record)
end
end
end
# Clears all records from the association. Returns an empty array.
def clear(klass = nil)
load_target
return if @target.empty?
if klass
delete(@target.select {|r| r.is_a? klass })
else
@owner.send(@reflection.through_reflection.name).clear
@target.clear
end
[]
end
protected
# undef :sum
# undef :create!
def construct_quoted_owner_attributes(*args) #:nodoc:
# no access to returning() here? why not?
type_key = @reflection.options[:foreign_type_key]
{@reflection.primary_key_name => @owner.id,
type_key=> (@owner.class.base_class.name if type_key)}
end
def construct_from #:nodoc:
# build the FROM part of the query, in this case, the polymorphic join table
@reflection.klass.table_name
end
def construct_owner #:nodoc:
# the table name for the owner object's class
@owner.class.table_name
end
def construct_owner_key #:nodoc:
# the primary key field for the owner object
@owner.class.primary_key
end
def construct_select(custom_select = nil) #:nodoc:
# build the select query
selected = custom_select || @reflection.options[:select]
end
def construct_joins(custom_joins = nil) #:nodoc:
# build the string of default joins
"JOIN #{construct_owner} polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " +
@reflection.options[:from].map do |plural|
klass = plural._as_class
"LEFT JOIN #{klass.table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}"
end.uniq.join(" ") + " #{custom_joins}"
end
def construct_conditions #:nodoc:
# build the fully realized condition string
conditions = construct_quoted_owner_attributes.map do |field, value|
"#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value
end
conditions << custom_conditions if custom_conditions
"(" + conditions.compact.join(') AND (') + ")"
end
def custom_conditions #:nodoc:
# custom conditions... not as messy as has_many :through because our joins are a little smarter
if @reflection.options[:conditions]
"(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")"
end
end
alias :construct_owner_attributes :construct_quoted_owner_attributes
alias :conditions :custom_conditions # XXX possibly not necessary
alias :sql_conditions :custom_conditions # XXX ditto
# construct attributes for join for a particular record
def construct_join_attributes(record) #:nodoc:
{@reflection.options[:polymorphic_key] => record.id,
@reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",
@reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ?
{@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships
end
def build(attrs = nil) #:nodoc:
raise PolymorphicMethodNotSupportedError, "You can't associate new records."
end
end
end
end

View file

@ -1,70 +0,0 @@
require 'initializer' unless defined? ::Rails::Initializer
require 'dispatcher' unless defined? ::ActionController::Dispatcher
module HasManyPolymorphs
=begin rdoc
Searches for models that use <tt>has_many_polymorphs</tt> or <tt>acts_as_double_polymorphic_join</tt> and makes sure that they get loaded during app initialization. This ensures that helper methods are injected into the target classes.
Note that you can override DEFAULT_OPTIONS via Rails::Configuration#has_many_polymorphs_options. For example, if you need an application extension to be required before has_many_polymorphs loads your models, add an <tt>after_initialize</tt> block in <tt>config/environment.rb</tt> that appends to the <tt>'requirements'</tt> key:
Rails::Initializer.run do |config|
# your other configuration here
config.after_initialize do
config.has_many_polymorphs_options['requirements'] << 'lib/my_extension'
end
end
=end
DEFAULT_OPTIONS = {
:file_pattern => "#{RAILS_ROOT}/app/models/**/*.rb",
:file_exclusions => ['svn', 'CVS', 'bzr'],
:methods => ['has_many_polymorphs', 'acts_as_double_polymorphic_join'],
:requirements => []}
mattr_accessor :options
@@options = HashWithIndifferentAccess.new(DEFAULT_OPTIONS)
# Dispatcher callback to load polymorphic relationships from the top down.
def self.autoload
_logger_debug "autoload hook invoked"
options[:requirements].each do |requirement|
_logger_warn "forcing requirement load of #{requirement}"
require requirement
end
Dir[options[:file_pattern]].each do |filename|
next if filename =~ /#{options[:file_exclusions].join("|")}/
open filename do |file|
if file.grep(/#{options[:methods].join("|")}/).any?
begin
model = File.basename(filename)[0..-4].camelize
_logger_warn "preloading parent model #{model}"
model.constantize
rescue Object => e
_logger_warn "#{model} could not be preloaded: #{e.inspect}"
end
end
end
end
end
end
class Rails::Initializer #:nodoc:
# Make sure it gets loaded in the console, tests, and migrations
def after_initialize_with_autoload
after_initialize_without_autoload
HasManyPolymorphs.autoload
end
alias_method_chain :after_initialize, :autoload
end
Dispatcher.to_prepare(:has_many_polymorphs_autoload) do
# Make sure it gets loaded in the app
HasManyPolymorphs.autoload
end

View file

@ -1,60 +0,0 @@
module ActiveRecord
class Base
class << self
# Interprets a polymorphic row from a unified SELECT, returning the appropriate ActiveRecord instance. Overrides ActiveRecord::Base.instantiate_without_callbacks.
def instantiate_with_polymorphic_checks(record)
if record['polymorphic_parent_class']
reflection = record['polymorphic_parent_class'].constantize.reflect_on_association(record['polymorphic_association_id'].to_sym)
# _logger_debug "Instantiating a polymorphic row for #{record['polymorphic_parent_class']}.reflect_on_association(:#{record['polymorphic_association_id']})"
# rewrite the record with the right column names
table_aliases = reflection.options[:table_aliases].dup
record = Hash[*table_aliases.keys.map {|key| [key, record[table_aliases[key]]] }.flatten]
# find the real child class
klass = record["#{self.table_name}.#{reflection.options[:polymorphic_type_key]}"].constantize
if sti_klass = record["#{klass.table_name}.#{klass.inheritance_column}"]
klass = klass.class_eval do compute_type(sti_klass) end # in case of namespaced STI models
end
# check that the join actually joined to something
unless (child_id = record["#{self.table_name}.#{reflection.options[:polymorphic_key]}"]) == record["#{klass.table_name}.#{klass.primary_key}"]
raise ActiveRecord::Associations::PolymorphicError,
"Referential integrity violation; child <#{klass.name}:#{child_id}> was not found for #{reflection.name.inspect}"
end
# eject the join keys
# XXX not very readable
record = Hash[*record._select do |column, value|
column[/^#{klass.table_name}/]
end.map do |column, value|
[column[/\.(.*)/, 1], value]
end.flatten]
# allocate and assign values
returning(klass.allocate) do |obj|
obj.instance_variable_set("@attributes", record)
obj.instance_variable_set("@attributes_cache", Hash.new)
if obj.respond_to_without_attributes?(:after_find)
obj.send(:callback, :after_find)
end
if obj.respond_to_without_attributes?(:after_initialize)
obj.send(:callback, :after_initialize)
end
end
else
instantiate_without_polymorphic_checks(record)
end
end
alias_method_chain :instantiate, :polymorphic_checks
end
end
end

View file

@ -1,596 +0,0 @@
module ActiveRecord #:nodoc:
module Associations #:nodoc:
=begin rdoc
Class methods added to ActiveRecord::Base for setting up polymorphic associations.
== Notes
STI association targets must enumerated and named. For example, if Dog and Cat both inherit from Animal, you still need to say <tt>[:dogs, :cats]</tt>, and not <tt>[:animals]</tt>.
Namespaced models follow the Rails <tt>underscore</tt> convention. ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
You do not need to set up any other associations other than for either the regular method or the double. The join associations and all individual and reverse associations are generated for you. However, a join model and table are required.
There is a tentative report that you can make the parent model be its own join model, but this is untested.
=end
module PolymorphicClassMethods
RESERVED_DOUBLES_KEYS = [:conditions, :order, :limit, :offset, :extend, :skip_duplicates,
:join_extend, :dependent, :rename_individual_collections,
:namespace] #:nodoc:
=begin rdoc
This method creates a doubled-sided polymorphic relationship. It must be called on the join model:
class Devouring < ActiveRecord::Base
belongs_to :eater, :polymorphic => true
belongs_to :eaten, :polymorphic => true
acts_as_double_polymorphic_join(
:eaters => [:dogs, :cats],
:eatens => [:cats, :birds]
)
end
The method works by defining one or more special <tt>has_many_polymorphs</tt> association on every model in the target lists, depending on which side of the association it is on. Double self-references will work.
The two association names and their value arrays are the only required parameters.
== Available options
These options are passed through to targets on both sides of the association. If you want to affect only one side, prepend the key with the name of that side. For example, <tt>:eaters_extend</tt>.
<tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, or <tt>:delete_all</tt>. Controls how the join record gets treated on any association delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
<tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
<tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with <tt>"\_of_#{association_name}"</tt>.
<tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
<tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
<tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
<tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
<tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
<tt>:offset</tt>:: An integer. Only affects the polymorphic association.
<tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
=end
def acts_as_double_polymorphic_join options={}, &extension
collections, options = extract_double_collections(options)
# handle the block
options[:extend] = (if options[:extend]
Array(options[:extend]) + [extension]
else
extension
end) if extension
collection_option_keys = make_general_option_keys_specific!(options, collections)
join_name = self.name.tableize.to_sym
collections.each do |association_id, children|
parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array
begin
parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
rescue NoMethodError
raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key
end
parents = collections[parent_hash_key]
conflicts = (children & parents) # set intersection
parents.each do |plural_parent_name|
parent_class = plural_parent_name._as_class
singular_reverse_association_id = parent_hash_key._singularize
internal_options = {
:is_double => true,
:from => children,
:as => singular_reverse_association_id,
:through => join_name.to_sym,
:foreign_key => parent_foreign_key,
:foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
:singular_reverse_association_id => singular_reverse_association_id,
:conflicts => conflicts
}
general_options = Hash[*options._select do |key, value|
collection_option_keys[association_id].include? key and !value.nil?
end.map do |key, value|
[key.to_s[association_id.to_s.length+1..-1].to_sym, value]
end._flatten_once] # rename side-specific options to general names
general_options.each do |key, value|
# avoid clobbering keys that appear in both option sets
if internal_options[key]
general_options[key] = Array(value) + Array(internal_options[key])
end
end
parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
if conflicts.include? plural_parent_name
# unify the alternate sides of the conflicting children
(conflicts).each do |method_name|
unless parent_class.instance_methods.include?(method_name)
parent_class.send(:define_method, method_name) do
(self.send("#{singular_reverse_association_id}_#{method_name}") +
self.send("#{association_id._singularize}_#{method_name}")).freeze
end
end
end
# unify the join model... join model is always renamed for doubles, unlike child associations
unless parent_class.instance_methods.include?(join_name)
parent_class.send(:define_method, join_name) do
(self.send("#{join_name}_as_#{singular_reverse_association_id}") +
self.send("#{join_name}_as_#{association_id._singularize}")).freeze
end
end
else
unless parent_class.instance_methods.include?(join_name)
parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}")
end
end
end
end
end
private
def extract_double_collections(options)
collections = options._select do |key, value|
value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
end
raise PolymorphicError, "Couldn't understand options in acts_as_double_polymorphic_join. Valid parameters are your two class collections, and then #{RESERVED_DOUBLES_KEYS.inspect[1..-2]}, with optionally your collection names prepended and joined with an underscore." unless collections.size == 2
options = options._select do |key, value|
!collections[key]
end
[collections, options]
end
def make_general_option_keys_specific!(options, collections)
collection_option_keys = Hash[*collections.keys.map do |key|
[key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}]
end._flatten_once]
collections.keys.each do |collection|
options.each do |key, value|
next if collection_option_keys.values.flatten.include? key
# shift the general options to the individual sides
collection_key = "#{collection}_#{key}".to_sym
collection_value = options[collection_key]
case key
when :conditions
collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
when :order
options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
when :extend, :join_extend
options[collection_key] = Array(collection_value) + Array(value)
else
options[collection_key] ||= value
end
end
end
collection_option_keys
end
public
=begin rdoc
This method createds a single-sided polymorphic relationship.
class Petfood < ActiveRecord::Base
has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
end
The only required parameter, aside from the association name, is <tt>:from</tt>.
The method generates a number of associations aside from the polymorphic one. In this example Petfood also gets <tt>dogs</tt>, <tt>cats</tt>, and <tt>birds</tt>, and Dog, Cat, and Bird get <tt>petfoods</tt>. (The reverse association to the parents is always plural.)
== Available options
<tt>:from</tt>:: An array of symbols representing the target models. Required.
<tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
<tt>:through</tt>:: A symbol representing the class of the join model. Follows Rails defaults if not supplied (the parent and the association names, alphabetized, concatenated with an underscore, and singularized).
<tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, <tt>:delete_all</tt>. Controls how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
<tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
<tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with "_of_#{association_name}"</tt>. For example, <tt>zoos</tt> becomes <tt>zoos_of_animals</tt>. This is to help avoid method name collisions in crowded classes.
<tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
<tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
<tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
<tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
<tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
<tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
<tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
<tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
<tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
<tt>:offset</tt>:: An integer. Only affects the polymorphic association.
<tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
<tt>:uniq</tt>:: If <tt>true</tt>, the records returned are passed through a pure-Ruby <tt>uniq</tt> before they are returned. Rarely needed.
<tt>:foreign_key</tt>:: The column name for the parent's id in the join.
<tt>:foreign_type_key</tt>:: The column name for the parent's class name in the join, if the parent itself is polymorphic. Rarely needed--if you're thinking about using this, you almost certainly want to use <tt>acts_as_double_polymorphic_join()</tt> instead.
<tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
<tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>.
== On condition nullification
When you request an individual association, non-applicable but fully-qualified fields in the polymorphic association's <tt>:conditions</tt>, <tt>:order</tt>, and <tt>:group</tt> options get changed to <tt>NULL</tt>. For example, if you set <tt>:conditions => "dogs.name != 'Spot'"</tt>, when you request <tt>.cats</tt>, the conditions string is changed to <tt>NULL != 'Spot'</tt>.
Be aware, however, that <tt>NULL != 'Spot'</tt> returns <tt>false</tt> due to SQL's 3-value logic. Instead, you need to use the <tt>:conditions</tt> string <tt>"dogs.name IS NULL OR dogs.name != 'Spot'"</tt> to get the behavior you probably expect for negative matches.
=end
def has_many_polymorphs (association_id, options = {}, &extension)
_logger_debug "associating #{self}.#{association_id}"
reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
# puts "Created reflection #{reflection.inspect}"
# configure_dependency_for_has_many(reflection)
collection_reader_method(reflection, PolymorphicAssociation)
end
# Composed method that assigns option defaults, builds the reflection object, and sets up all the related associations on the parent, join, and targets.
def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
options.assert_valid_keys(
:from,
:as,
:through,
:foreign_key,
:foreign_type_key,
:polymorphic_key, # same as :association_foreign_key
:polymorphic_type_key,
:dependent, # default :destroy, only affects the join table
:skip_duplicates, # default true, only affects the polymorphic collection
:ignore_duplicates, # deprecated
:is_double,
:rename_individual_collections,
:reverse_association_id, # not used
:singular_reverse_association_id,
:conflicts,
:extend,
:join_class_name,
:join_extend,
:parent_extend,
:table_aliases,
:select, # applies to the polymorphic relationship
:conditions, # applies to the polymorphic relationship, the children, and the join
# :include,
:parent_conditions,
:parent_order,
:order, # applies to the polymorphic relationship, the children, and the join
:group, # only applies to the polymorphic relationship and the children
:limit, # only applies to the polymorphic relationship and the children
:offset, # only applies to the polymorphic relationship
:parent_order,
:parent_group,
:parent_limit,
:parent_offset,
# :source,
:namespace,
:uniq, # XXX untested, only applies to the polymorphic relationship
# :finder_sql,
# :counter_sql,
# :before_add,
# :after_add,
# :before_remove,
# :after_remove
:dummy)
# validate against the most frequent configuration mistakes
verify_pluralization_of(association_id)
raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array
options[:from].each{|plural| verify_pluralization_of(plural)}
options[:as] ||= self.name.demodulize.underscore.to_sym
options[:conflicts] = Array(options[:conflicts])
options[:foreign_key] ||= "#{options[:as]}_id"
options[:association_foreign_key] =
options[:polymorphic_key] ||= "#{association_id._singularize}_id"
options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
if options.has_key? :ignore_duplicates
_logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
options[:skip_duplicates] = options[:ignore_duplicates]
end
options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
options[:dependent] = :destroy unless options.has_key? :dependent
options[:conditions] = sanitize_sql(options[:conditions])
# options[:finder_sql] ||= "(options[:polymorphic_key]
options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
# set up namespaces if we have a namespace key
# XXX needs test coverage
if options[:namespace]
namespace = options[:namespace].to_s.chomp("/") + "/"
options[:from].map! do |child|
"#{namespace}#{child}".to_sym
end
options[:through] = "#{namespace}#{options[:through]}".to_sym
end
options[:join_class_name] ||= options[:through]._classify
options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
options[:select] ||= build_select(association_id, options[:table_aliases])
options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
options[:through] = demodulate(options[:through]).to_sym
options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")
# create the reflection object
returning(create_reflection(:has_many_polymorphs, association_id, options, self)) do |reflection|
# set up the other related associations
create_join_association(association_id, reflection)
create_has_many_through_associations_for_parent_to_children(association_id, reflection)
create_has_many_through_associations_for_children_to_parent(association_id, reflection)
end
end
private
# table mapping for use at the instantiation point
def build_table_aliases(from)
# for the targets
returning({}) do |aliases|
from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
begin
table = plural._as_class.table_name
rescue NameError => e
raise PolymorphicError, "Could not find a valid class for #{plural.inspect}. If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead."
end
plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
end
end
end
end
def build_select(association_id, aliases)
# <tt>instantiate</tt> has to know which reflection the results are coming from
(["\'#{self.name}\' AS polymorphic_parent_class",
"\'#{association_id}\' AS polymorphic_association_id"] +
aliases.map do |table, _alias|
"#{table} AS #{_alias}"
end.sort).join(", ")
end
# method sub-builders
def create_join_association(association_id, reflection)
options = {
:foreign_key => reflection.options[:foreign_key],
:dependent => reflection.options[:dependent],
:class_name => reflection.klass.name,
:extend => reflection.options[:join_extend]
# :limit => reflection.options[:limit],
# :offset => reflection.options[:offset],
# :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
# :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
}
if reflection.options[:foreign_type_key]
type_check = "#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
conjunction = options[:conditions] ? " AND " : nil
options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
options[:as] = reflection.options[:as]
end
has_many(reflection.options[:through], options)
inject_before_save_into_join_table(association_id, reflection)
end
def inject_before_save_into_join_table(association_id, reflection)
sti_hook = "sti_class_rewrite"
rewrite_procedure = %[self.send(:#{reflection.options[:polymorphic_type_key]}=, self.#{reflection.options[:polymorphic_type_key]}.constantize.base_class.name)]
# XXX should be abstracted?
reflection.klass.class_eval %[
unless instance_methods.include? "before_save_with_#{sti_hook}"
if instance_methods.include? "before_save"
alias_method :before_save_without_#{sti_hook}, :before_save
def before_save_with_#{sti_hook}
before_save_without_#{sti_hook}
#{rewrite_procedure}
end
else
def before_save_with_#{sti_hook}
#{rewrite_procedure}
end
end
alias_method :before_save, :before_save_with_#{sti_hook}
end
]
end
def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
child_pluralization_map(association_id, reflection).each do |plural, singular|
if singular == reflection.options[:as]
raise PolymorphicError, if reflection.options[:is_double]
"You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
else
"You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
end
end
parent = self
plural._as_class.instance_eval do
# this shouldn't be called at all during doubles; there is no way to traverse to a double polymorphic parent (XXX is that right?)
unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym
# the join table
through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym
has_many(through,
:as => association_id._singularize,
# :source => association_id._singularize,
# :source_type => reflection.options[:polymorphic_type_key],
:class_name => reflection.klass.name,
:dependent => reflection.options[:dependent],
:extend => reflection.options[:join_extend],
# :limit => reflection.options[:limit],
# :offset => reflection.options[:offset],
:order => devolve(association_id, reflection, reflection.options[:parent_order], reflection.klass),
:conditions => devolve(association_id, reflection, reflection.options[:parent_conditions], reflection.klass)
)
# the association to the target's parents
association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym
has_many(association,
:through => through,
:class_name => parent.name,
:source => reflection.options[:as],
:foreign_key => reflection.options[:foreign_key],
:extend => reflection.options[:parent_extend],
:conditions => reflection.options[:parent_conditions],
:order => reflection.options[:parent_order],
:offset => reflection.options[:parent_offset],
:limit => reflection.options[:parent_limit],
:group => reflection.options[:parent_group])
# debugger if association == :parents
#
# nil
end
end
end
end
def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
child_pluralization_map(association_id, reflection).each do |plural, singular|
#puts ":source => #{child}"
current_association = demodulate(child_association_map(association_id, reflection)[plural])
source = demodulate(singular)
if reflection.options[:conflicts].include? plural
# XXX check this
current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
source = "#{source}_as_#{association_id._singularize}".to_sym
end
# make push/delete accessible from the individual collections but still operate via the general collection
extension_module = self.class_eval %[
module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end
alias :<< :push
def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
self
end]
has_many(current_association.to_sym,
:through => reflection.options[:through],
:source => association_id._singularize,
:source_type => plural._as_class.base_class.name,
:class_name => plural._as_class.name, # make STI not conflate subtypes
:extend => (Array(extension_module) + reflection.options[:extend]),
:limit => reflection.options[:limit],
# :offset => reflection.options[:offset],
:order => devolve(association_id, reflection, reflection.options[:order], plural._as_class),
:conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class),
:group => devolve(association_id, reflection, reflection.options[:group], plural._as_class)
)
end
end
# some support methods
def child_pluralization_map(association_id, reflection)
Hash[*reflection.options[:from].map do |plural|
[plural, plural._singularize]
end.flatten]
end
def child_association_map(association_id, reflection)
Hash[*reflection.options[:from].map do |plural|
[plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
end.flatten]
end
def demodulate(s)
s.to_s.gsub('/', '_').to_sym
end
def build_join_table_symbol(association_id, name)
[name.to_s, association_id.to_s].sort.join("_").to_sym
end
def all_classes_for(association_id, reflection)
klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
klasses += klasses.map(&:base_class)
klasses.uniq
end
def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false)
# XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
return unless string
string = string.dup
# _logger_debug "devolving #{string} for #{klass}"
inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
[klass, klass.base_class, reflection.klass, reflection.klass.base_class])
inappropriate_classes.map do |klass|
klass.columns.map do |column|
[klass.table_name, column.name]
end.map do |table, column|
["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
end
end.flatten.sort_by(&:size).reverse.each do |quoted_reference|
# _logger_debug "devolved #{quoted_reference} to NULL"
# XXX clause removal would go here
string.gsub!(quoted_reference, "NULL")
end
# _logger_debug "altered to #{string}"
string
end
def verify_pluralization_of(sym)
sym = sym.to_s
singular = sym.singularize
plural = singular.pluralize
raise PolymorphicError, "Pluralization rules not set up correctly. You passed :#{sym}, which singularizes to :#{singular}, but that pluralizes to :#{plural}, which is different. Maybe you meant :#{plural} to begin with?" unless sym == plural
end
def spiked_create_extension_module(association_id, extensions, identifier = nil)
module_extensions = extensions.select{|e| e.is_a? Module}
proc_extensions = extensions.select{|e| e.is_a? Proc }
# support namespaced anonymous blocks as well as multiple procs
proc_extensions.each_with_index do |proc_extension, index|
module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
the_module.class_eval &proc_extension
module_extensions << the_module
end
module_extensions
end
end
end
end

View file

@ -1,19 +0,0 @@
=begin rdoc
Access the <tt>has_many_polymorphs_options</tt> hash in your Rails::Initializer.run#after_initialize block if you need to modify the behavior of Rails::Initializer::HasManyPolymorphsAutoload.
=end
module Rails #:nodoc:
class Configuration
def has_many_polymorphs_options
::HasManyPolymorphs.options
end
def has_many_polymorphs_options=(hash)
::HasManyPolymorphs.options = HashWithIndifferentAccess.new(hash)
end
end
end

View file

@ -1,103 +0,0 @@
=begin rdoc
Debugging tools for Has_many_polymorphs.
Enable the different tools by setting the environment variable HMP_DEBUG. Settings with special meaning are <tt>"ruby-debug"</tt>, <tt>"trace"</tt>, and <tt>"dependencies"</tt>.
== Code generation
Enabled by default when HMP_DEBUG is set.
Ouputs a folder <tt>generated_models/</tt> in RAILS_ROOT containing valid Ruby files explaining all the ActiveRecord relationships set up by the plugin, as well as listing the line in the plugin that called each particular association method.
== Ruby-debug
Enable by setting HMP_DEBUG to <tt>"ruby-debug"</tt>.
Starts <tt>ruby-debug</tt> for the life of the process.
== Trace
Enable by setting HMP_DEBUG to <tt>"ruby-debug"</tt>.
Outputs an indented trace of relevant method calls as they occur.
== Dependencies
Enable by setting HMP_DEBUG to <tt>"dependencies"</tt>.
Turns on Rails' default dependency logging.
=end
_logger_warn "debug mode enabled"
class << ActiveRecord::Base
COLLECTION_METHODS = [:belongs_to, :has_many, :has_and_belongs_to_many, :has_one,
:has_many_polymorphs, :acts_as_double_polymorphic_join].each do |method_name|
alias_method "original_#{method_name}".to_sym, method_name
undef_method method_name
end
unless defined? GENERATED_CODE_DIR
GENERATED_CODE_DIR = "#{RAILS_ROOT}/generated_models"
begin
system "rm -rf #{GENERATED_CODE_DIR}"
Dir.mkdir GENERATED_CODE_DIR
rescue Errno::EACCES
_logger_warn "no permissions for generated code dir: #{GENERATED_CODE_DIR}"
end
if File.exist? GENERATED_CODE_DIR
alias :original_method_missing :method_missing
def method_missing(method_name, *args, &block)
if COLLECTION_METHODS.include? method_name.to_sym
Dir.chdir GENERATED_CODE_DIR do
filename = "#{demodulate(self.name.underscore)}.rb"
contents = File.open(filename).read rescue "\nclass #{self.name}\n\nend\n"
line = caller[1][/\:(\d+)\:/, 1]
contents[-5..-5] = "\n #{method_name} #{args[0..-2].inspect[1..-2]},\n #{args[-1].inspect[1..-2].gsub(" :", "\n :").gsub("=>", " => ")}\n#{ block ? " #{block.inspect.sub(/\@.*\//, '@')}\n" : ""} # called from line #{line}\n\n"
File.open(filename, "w") do |file|
file.puts contents
end
end
# doesn't actually display block contents
self.send("original_#{method_name}", *args, &block)
else
self.send(:original_method_missing, method_name, *args, &block)
end
end
end
end
end
case ENV['HMP_DEBUG']
when "ruby-debug"
require 'rubygems'
require 'ruby-debug'
Debugger.start
_logger_warn "ruby-debug enabled."
when "trace"
_logger_warn "method tracing enabled"
$debug_trace_indent = 0
set_trace_func (proc do |event, file, line, id, binding, classname|
if id.to_s =~ /instantiate/ #/IRB|Wirble|RubyLex|RubyToken|Logger|ConnectionAdapters|SQLite3|MonitorMixin|Benchmark|Inflector|Inflections/
if event == 'call'
puts (" " * $debug_trace_indent) + "#{event}ed #{classname}\##{id} from #{file.split('/').last}::#{line}"
$debug_trace_indent += 1
elsif event == 'return'
$debug_trace_indent -= 1 unless $debug_trace_indent == 0
puts (" " * $debug_trace_indent) + "#{event}ed #{classname}\##{id}"
end
end
end)
when "dependencies"
_logger_warn "dependency activity being logged"
(::Dependencies.log_activity = true) rescue nil
end

View file

@ -1,35 +0,0 @@
# Redefine instead of chain a Rake task
# http://www.bigbold.com/snippets/posts/show/2032
module Rake
module TaskManager
def redefine_task(task_class, args, &block)
task_name, deps = resolve_args(args)
task_name = task_class.scope_name(@scope, task_name)
deps = [deps] unless deps.respond_to?(:to_ary)
deps = deps.collect {|d| d.to_s }
task = @tasks[task_name.to_s] = task_class.new(task_name, self)
task.application = self
task.add_comment(@last_comment)
@last_comment = nil
task.enhance(deps, &block)
task
end
end
class Task
class << self
def redefine_task(args, &block)
Rake.application.redefine_task(self, args, &block)
end
end
end
end
class Object
def silently
stderr, stdout, $stderr, $stdout = $stderr, $stdout, StringIO.new, StringIO.new
yield
$stderr, $stdout = stderr, stdout
end
end

View file

@ -1,58 +0,0 @@
module ActiveRecord #:nodoc:
module Reflection #:nodoc:
module ClassMethods #:nodoc:
# Update the default reflection switch so that <tt>:has_many_polymorphs</tt> types get instantiated.
# It's not a composed method so we have to override the whole thing.
def create_reflection(macro, name, options, active_record)
case macro
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
klass = options[:through] ? ThroughReflection : AssociationReflection
reflection = klass.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
# added by has_many_polymorphs #
when :has_many_polymorphs
reflection = PolymorphicReflection.new(macro, name, options, active_record)
end
write_inheritable_hash :reflections, name => reflection
reflection
end
end
class PolymorphicError < ActiveRecordError #:nodoc:
end
=begin rdoc
The reflection built by the <tt>has_many_polymorphs</tt> method.
Inherits from ActiveRecord::Reflection::AssociationReflection.
=end
class PolymorphicReflection < AssociationReflection
# Stub out the validity check. Has_many_polymorphs checks validity on macro creation, not on reflection.
def check_validity!
# nothing
end
# Return the source reflection.
def source_reflection
# normally is the has_many to the through model, but we return ourselves,
# since there isn't a real source class for a polymorphic target
self
end
# Set the classname of the target. Uses the join class name.
def class_name
# normally is the classname of the association target
@class_name ||= options[:join_class_name]
end
end
end
end

View file

@ -1,84 +0,0 @@
class String
# Changes an underscored string into a class reference.
def _as_class
# classify expects self to be plural
self.classify.constantize
end
# For compatibility with the Symbol extensions.
alias :_singularize :singularize
alias :_pluralize :pluralize
alias :_classify :classify
end
class Symbol
# Changes an underscored symbol into a class reference.
def _as_class; self.to_s._as_class; end
# Changes a plural symbol into a singular symbol.
def _singularize; self.to_s.singularize.to_sym; end
# Changes a singular symbol into a plural symbol.
def _pluralize; self.to_s.pluralize.to_sym; end
# Changes a symbol into a class name string.
def _classify; self.to_s.classify; end
end
class Array
# Flattens the first level of self.
def _flatten_once
self.inject([]){|r, el| r + Array(el)}
end
# Rails 1.2.3 compatibility method. Copied from http://dev.rubyonrails.org/browser/trunk/activesupport/lib/active_support/core_ext/array/extract_options.rb?rev=7217
def _extract_options!
last.is_a?(::Hash) ? pop : {}
end
end
class Hash
# An implementation of select that returns a Hash.
def _select
Hash[*self.select do |key, value|
yield key, value
end._flatten_once]
end
end
class Object
# Returns the metaclass of self.
def _metaclass; (class << self; self; end); end
# Logger shortcut.
def _logger_debug s
s = "** has_many_polymorphs: #{s}"
RAILS_DEFAULT_LOGGER.debug(s) if RAILS_DEFAULT_LOGGER
end
# Logger shortcut.
def _logger_warn s
s = "** has_many_polymorphs: #{s}"
if RAILS_DEFAULT_LOGGER
RAILS_DEFAULT_LOGGER.warn(s)
else
$stderr.puts(s)
end
end
end
class ActiveRecord::Base
# Return the base class name as a string.
def _base_class_name
self.class.base_class.name.to_s
end
end