kronn-has_many_polymorphs 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'active_record'
2
+ require 'has_many_polymorphs/reflection'
3
+ require 'has_many_polymorphs/association'
4
+ require 'has_many_polymorphs/class_methods'
5
+
6
+ require 'has_many_polymorphs/support_methods'
7
+ require 'has_many_polymorphs/base'
8
+
9
+ class ActiveRecord::Base
10
+ extend ActiveRecord::Associations::PolymorphicClassMethods
11
+ end
12
+
13
+ if ENV['HMP_DEBUG'] && (Rails.env.development? || Rails.env.test?)
14
+ require 'has_many_polymorphs/debugging_tools'
15
+ end
16
+
17
+ require 'has_many_polymorphs/railtie'
18
+
19
+ _logger_debug "loaded ok"
@@ -0,0 +1,165 @@
1
+ module ActiveRecord #:nodoc:
2
+ module Associations #:nodoc:
3
+
4
+ class PolymorphicError < ActiveRecordError #:nodoc:
5
+ end
6
+
7
+ class PolymorphicMethodNotSupportedError < ActiveRecordError #:nodoc:
8
+ end
9
+
10
+ # The association class for a <tt>has_many_polymorphs</tt> association.
11
+ class PolymorphicAssociation < HasManyThroughAssociation
12
+
13
+ # 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.
14
+ def <<(*records)
15
+ return if records.empty?
16
+
17
+ if @reflection.options[:skip_duplicates]
18
+ _logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue"
19
+ load_target
20
+ end
21
+
22
+ @reflection.klass.transaction do
23
+ flatten_deeper(records).each do |record|
24
+ if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record?
25
+ raise PolymorphicError, "You can't associate unsaved records."
26
+ end
27
+ next if @reflection.options[:skip_duplicates] and @target.include? record
28
+ @owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record))
29
+ @target << record if loaded?
30
+ end
31
+ end
32
+
33
+ self
34
+ end
35
+
36
+ alias :push :<<
37
+ alias :concat :<<
38
+
39
+ # 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.
40
+ def find(*args)
41
+ opts = args._extract_options!
42
+ opts.delete :include
43
+ super(*(args + [opts]))
44
+ end
45
+
46
+ def construct_scope
47
+ _logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet."
48
+ super
49
+ end
50
+
51
+ # Deletes a record from the association. Return value is undefined.
52
+ def delete(*records)
53
+ records = flatten_deeper(records)
54
+ records.reject! {|record| @target.delete(record) if record.new_record?}
55
+ return if records.empty?
56
+
57
+ @reflection.klass.transaction do
58
+ records.each do |record|
59
+ joins = @reflection.through_reflection.name
60
+ @owner.send(joins).delete(@owner.send(joins).select do |join|
61
+ join.send(@reflection.options[:polymorphic_key]) == record.id and
62
+ join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}"
63
+ end)
64
+ @target.delete(record)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Clears all records from the association. Returns an empty array.
70
+ def clear(klass = nil)
71
+ load_target
72
+ return if @target.empty?
73
+
74
+ if klass
75
+ delete(@target.select {|r| r.is_a? klass })
76
+ else
77
+ @owner.send(@reflection.through_reflection.name).clear
78
+ @target.clear
79
+ end
80
+ []
81
+ end
82
+
83
+ protected
84
+
85
+ # undef :sum
86
+ # undef :create!
87
+
88
+ def construct_quoted_owner_attributes(*args) #:nodoc:
89
+ # no access to returning() here? why not?
90
+ type_key = @reflection.options[:foreign_type_key]
91
+ h = {@reflection.primary_key_name => @owner.id}
92
+ h[type_key] = @owner.class.base_class.name if type_key
93
+ h
94
+ end
95
+
96
+ def construct_from #:nodoc:
97
+ # build the FROM part of the query, in this case, the polymorphic join table
98
+ @reflection.klass.quoted_table_name
99
+ end
100
+
101
+ def construct_owner #:nodoc:
102
+ # the table name for the owner object's class
103
+ @owner.class.quoted_table_name
104
+ end
105
+
106
+ def construct_owner_key #:nodoc:
107
+ # the primary key field for the owner object
108
+ @owner.class.primary_key
109
+ end
110
+
111
+ def construct_select(custom_select = nil) #:nodoc:
112
+ # build the select query
113
+ selected = custom_select || @reflection.options[:select]
114
+ end
115
+
116
+ def construct_joins(custom_joins = nil) #:nodoc:
117
+ # build the string of default joins
118
+ "JOIN #{construct_owner} AS polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " +
119
+ @reflection.options[:from].map do |plural|
120
+ klass = plural._as_class
121
+ "LEFT JOIN #{klass.quoted_table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.quoted_table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}"
122
+ end.uniq.join(" ") + " #{custom_joins}"
123
+ end
124
+
125
+ def construct_conditions #:nodoc:
126
+ # build the fully realized condition string
127
+ conditions = construct_quoted_owner_attributes.map do |field, value|
128
+ "#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value
129
+ end
130
+ conditions << custom_conditions if custom_conditions
131
+ "(" + conditions.compact.join(') AND (') + ")"
132
+ end
133
+
134
+ def custom_conditions #:nodoc:
135
+ # custom conditions... not as messy as has_many :through because our joins are a little smarter
136
+ if @reflection.options[:conditions]
137
+ "(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")"
138
+ end
139
+ end
140
+
141
+ alias :construct_owner_attributes :construct_quoted_owner_attributes
142
+ alias :conditions :custom_conditions # XXX possibly not necessary
143
+ alias :sql_conditions :custom_conditions # XXX ditto
144
+
145
+ # construct attributes for join for a particular record
146
+ def construct_join_attributes(record) #:nodoc:
147
+ {
148
+ @reflection.options[:polymorphic_key] => record.id,
149
+ @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",
150
+ @reflection.options[:foreign_key] => @owner.id
151
+ }.merge(
152
+ @reflection.options[:foreign_type_key] ?
153
+ { @reflection.options[:foreign_type_key] => "#{@owner.class.base_class}" } :
154
+ {}
155
+ ) # for double-sided relationships
156
+ end
157
+
158
+ def build(attrs = nil) #:nodoc:
159
+ raise PolymorphicMethodNotSupportedError, "You can't associate new records."
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,58 @@
1
+ require "#{Rails.root}/config/initializers/inflections"
2
+
3
+ module HasManyPolymorphs
4
+
5
+ =begin rdoc
6
+ 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.
7
+
8
+ 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:
9
+ Rails::Initializer.run do |config|
10
+ # your other configuration here
11
+
12
+ config.after_initialize do
13
+ config.has_many_polymorphs.options['requirements'] << 'lib/my_extension'
14
+ end
15
+ end
16
+
17
+ =end
18
+
19
+ MODELS_ROOT = Rails.root.join('app', 'models')
20
+
21
+ DEFAULT_OPTIONS = {
22
+ :file_pattern => "#{MODELS_ROOT}/**/*.rb",
23
+ :file_exclusions => ['svn', 'CVS', 'bzr'],
24
+ :methods => ['has_many_polymorphs', 'acts_as_double_polymorphic_join'],
25
+ :requirements => []}
26
+
27
+ mattr_accessor :options
28
+ @@options = HashWithIndifferentAccess.new(DEFAULT_OPTIONS)
29
+
30
+ # Dispatcher callback to load polymorphic relationships from the top down.
31
+ def self.setup
32
+
33
+ _logger_debug "autoload hook invoked"
34
+
35
+ options[:requirements].each do |requirement|
36
+ _logger_warn "forcing requirement load of #{requirement}"
37
+ require requirement
38
+ end
39
+
40
+ Dir.glob(options[:file_pattern]).each do |filename|
41
+ next if filename =~ /#{options[:file_exclusions].join("|")}/
42
+ open(filename) do |file|
43
+ if file.grep(/#{options[:methods].join("|")}/).any?
44
+ begin
45
+ # determines the modelname by the directory - this allows the autoload of namespaced models
46
+ modelname = filename[0..-4].gsub("#{MODELS_ROOT.to_s}/", "")
47
+ model = modelname.camelize
48
+ _logger_warn "preloading parent model #{model}"
49
+ model.constantize
50
+ rescue Object => e
51
+ _logger_warn "#{model} could not be preloaded: #{e.inspect} #{e.backtrace}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,71 @@
1
+ module ActiveRecord
2
+ class Base
3
+ class << self
4
+ # Interprets a polymorphic row from a unified SELECT, returning the
5
+ # appropriate ActiveRecord instance. Overrides
6
+ # ActiveRecord::Base.instantiate_without_callbacks.
7
+ def instantiate_with_polymorphic_checks(record)
8
+ if record['polymorphic_parent_class']
9
+ reflection = record['polymorphic_parent_class'].constantize.reflect_on_association(
10
+ record['polymorphic_association_id'].to_sym
11
+ )
12
+ # _logger_debug "Instantiating a polymorphic row for #{
13
+ # record['polymorphic_parent_class']
14
+ # }.reflect_on_association(:#{
15
+ # record['polymorphic_association_id']
16
+ # })"
17
+
18
+ # rewrite the record with the right column names
19
+ table_aliases = reflection.options[:table_aliases].dup
20
+ record = Hash[*table_aliases.keys.map {|key| [key, record[table_aliases[key]]] }.flatten]
21
+
22
+ # find the real child class
23
+ klass = record["#{self.table_name}.#{reflection.options[:polymorphic_type_key]}"].constantize
24
+ if sti_klass = record["#{klass.table_name}.#{klass.inheritance_column}"]
25
+ klass = klass.class_eval do compute_type(sti_klass) end # in case of namespaced STI models
26
+
27
+ # copy the data over to the right structure
28
+ klass.columns.map(&:name).each do |col|
29
+ record["#{klass.table_name}.#{col}"] = record["#{klass.base_class.table_name}.#{col}"]
30
+ end
31
+ end
32
+
33
+ # check that the join actually joined to something
34
+ child_id = record["#{self.table_name}.#{reflection.options[:polymorphic_key]}"]
35
+ unless child_id == record["#{klass.table_name}.#{klass.primary_key}"]
36
+ msg = []
37
+ msg << "Referential integrity violation"
38
+ msg << "child <#{klass.name}:#{child_id}> was not found for #{reflection.name.inspect}"
39
+ raise ActiveRecord::Associations::PolymorphicError, msg.join('; ')
40
+ end
41
+
42
+ # eject the join keys
43
+ # XXX not very readable
44
+ record = Hash[*record._select do |column, value|
45
+ column[/^#{klass.table_name}/]
46
+ end.map do |column, value|
47
+ [column[/\.(.*)/, 1], value]
48
+ end.flatten]
49
+
50
+ # allocate and assign values
51
+ klass.allocate.tap do |obj|
52
+ obj.instance_variable_set("@attributes", record)
53
+ obj.instance_variable_set("@attributes_cache", Hash.new)
54
+
55
+ if obj.respond_to_without_attributes?(:after_find)
56
+ obj.send(:callback, :after_find)
57
+ end
58
+
59
+ if obj.respond_to_without_attributes?(:after_initialize)
60
+ obj.send(:callback, :after_initialize)
61
+ end
62
+
63
+ end
64
+ else
65
+ instantiate_without_polymorphic_checks(record)
66
+ end
67
+ end
68
+ alias_method_chain :instantiate, :polymorphic_checks
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,598 @@
1
+ module ActiveRecord #:nodoc:
2
+ module Associations #:nodoc:
3
+ # Class methods added to ActiveRecord::Base for setting up
4
+ # polymorphic associations
5
+ #
6
+ # == Notes
7
+ #
8
+ # STI association targets must enumerated and named. For example,
9
+ # if Dog and Cat both inherit from Animal, you still need to say
10
+ # <tt>[:dogs, :cats]</tt>, and not <tt>[:animals]</tt>.
11
+ #
12
+ # Namespaced models follow the Rails <tt>underscore</tt> convention.
13
+ # ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
14
+ #
15
+ # You do not need to set up any other associations other than for
16
+ # either the regular method or the double. The join associations and
17
+ # all individual and reverse associations are generated for you.
18
+ # However, a join model and table are required.
19
+ #
20
+ # There is a tentative report that you can make the parent model be
21
+ # its own join model, but this is untested.
22
+ module PolymorphicClassMethods
23
+ RESERVED_DOUBLES_KEYS = [
24
+ :conditions, :order, :limit, :offset, :extend,
25
+ :skip_duplicates, :join_extend, :dependent,
26
+ :rename_individual_collections, :namespace
27
+ ] #:nodoc:
28
+
29
+ # This method creates a doubled-sided polymorphic relationship. It must be called on the join model:
30
+ #
31
+ # class Devouring < ActiveRecord::Base
32
+ # belongs_to :eater, :polymorphic => true
33
+ # belongs_to :eaten, :polymorphic => true
34
+ #
35
+ # acts_as_double_polymorphic_join(
36
+ # :eaters => [:dogs, :cats],
37
+ # :eatens => [:cats, :birds]
38
+ # )
39
+ # end
40
+ #
41
+ # The method works by defining one or more special
42
+ # <tt>has_many_polymorphs</tt> association on every model in the target
43
+ # lists, depending on which side of the association it is on. Double
44
+ # self-references will work.
45
+ #
46
+ # The two association names and their value arrays are the only required
47
+ # parameters.
48
+ #
49
+ # == Available options
50
+ #
51
+ # 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>.
52
+ #
53
+ # <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>.
54
+ # <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>.
55
+ # <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>.
56
+ # <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
57
+ # <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
58
+ # <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
59
+ # <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
60
+ # <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
61
+ # <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
62
+ # <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.
63
+ def acts_as_double_polymorphic_join options={}, &extension
64
+ collections, options = extract_double_collections(options)
65
+
66
+ # handle the block
67
+ options[:extend] = (if options[:extend]
68
+ Array(options[:extend]) + [extension]
69
+ else
70
+ extension
71
+ end) if extension
72
+
73
+ collection_option_keys = make_general_option_keys_specific!(options, collections)
74
+
75
+ join_name = self.name.tableize.to_sym
76
+ collections.each do |association_id, children|
77
+ parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array
78
+
79
+ begin
80
+ parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
81
+ rescue NoMethodError
82
+ unless parent_foreign_key
83
+ msg = "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}."
84
+ raise PolymorphicError, msg
85
+ end
86
+ end
87
+
88
+ parents = collections[parent_hash_key]
89
+ conflicts = (children & parents) # set intersection
90
+ parents.each do |plural_parent_name|
91
+
92
+ parent_class = plural_parent_name._as_class
93
+ singular_reverse_association_id = parent_hash_key._singularize
94
+
95
+ internal_options = {
96
+ :is_double => true,
97
+ :from => children,
98
+ :as => singular_reverse_association_id,
99
+ :through => join_name.to_sym,
100
+ :foreign_key => parent_foreign_key,
101
+ :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
102
+ :singular_reverse_association_id => singular_reverse_association_id,
103
+ :conflicts => conflicts
104
+ }
105
+
106
+ general_options = Hash[*options._select do |key, value|
107
+ collection_option_keys[association_id].include? key and !value.nil?
108
+ end.map do |key, value|
109
+ [key.to_s[association_id.to_s.length+1..-1].to_sym, value]
110
+ end._flatten_once] # rename side-specific options to general names
111
+
112
+ general_options.each do |key, value|
113
+ # avoid clobbering keys that appear in both option sets
114
+ if internal_options[key]
115
+ general_options[key] = Array(value) + Array(internal_options[key])
116
+ end
117
+ end
118
+
119
+ parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
120
+
121
+ if conflicts.include? plural_parent_name
122
+ # unify the alternate sides of the conflicting children
123
+ (conflicts).each do |method_name|
124
+ unless parent_class.instance_methods.include?(method_name)
125
+ parent_class.send(:define_method, method_name) do
126
+ (self.send("#{singular_reverse_association_id}_#{method_name}") +
127
+ self.send("#{association_id._singularize}_#{method_name}")).freeze
128
+ end
129
+ end
130
+ end
131
+
132
+ # unify the join model... join model is always renamed for doubles, unlike child associations
133
+ unless parent_class.instance_methods.include?(join_name)
134
+ parent_class.send(:define_method, join_name) do
135
+ (self.send("#{join_name}_as_#{singular_reverse_association_id}") +
136
+ self.send("#{join_name}_as_#{association_id._singularize}")).freeze
137
+ end
138
+ end
139
+ else
140
+ unless parent_class.instance_methods.include?(join_name)
141
+ # ensure there are no forward slashes in the aliased join_name_method (occurs when namespaces are used)
142
+ join_name_method = join_name.to_s.gsub('/', '_').to_sym
143
+ parent_class.send(:alias_method, join_name_method, "#{join_name_method}_as_#{singular_reverse_association_id}")
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ # This method createds a single-sided polymorphic relationship
151
+ #
152
+ # class Petfood < ActiveRecord::Base has_many_polymorphs :eaters,
153
+ # :from => [:dogs, :cats, :birds] end
154
+ #
155
+ # The only required parameter, aside from the association name, is
156
+ # <tt>:from</tt>.
157
+ #
158
+ # The method generates a number of associations aside from the
159
+ # polymorphic one. In this example Petfood also gets <tt>dogs</tt>,
160
+ # <tt>cats</tt>, and <tt>birds</tt>, and Dog, Cat, and Bird get
161
+ # <tt>petfoods</tt>. (The reverse association to the parents is
162
+ # always plural.)
163
+ #
164
+ # == Available options
165
+ #
166
+ # <tt>:from</tt>:: An array of symbols representing the target models. Required.
167
+ # <tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
168
+ # <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).
169
+ # <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>.
170
+ # <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>.
171
+ # <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.
172
+ # <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
173
+ # <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
174
+ # <tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
175
+ # <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
176
+ # <tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
177
+ # <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
178
+ # <tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
179
+ # <tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
180
+ # <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
181
+ # <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
182
+ # <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.
183
+ # <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.
184
+ # <tt>:foreign_key</tt>:: The column name for the parent's id in the join.
185
+ # <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.
186
+ # <tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
187
+ # <tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
188
+ #
189
+ # If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>.
190
+ #
191
+ # == On condition nullification
192
+ #
193
+ # 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>.
194
+ #
195
+ # 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.
196
+ def has_many_polymorphs(association_id, options = {}, &extension)
197
+ _logger_debug "associating #{self}.#{association_id}"
198
+ reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
199
+ # puts "Created reflection #{reflection.inspect}"
200
+ # configure_dependency_for_has_many(reflection)
201
+ collection_reader_method(reflection, PolymorphicAssociation)
202
+ end
203
+
204
+ # Composed method that assigns option defaults, builds the
205
+ # reflection object, and sets up all the related associations on
206
+ # the parent, join, and targets.
207
+ def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
208
+ options.assert_valid_keys(
209
+ :from,
210
+ :as,
211
+ :through,
212
+ :foreign_key,
213
+ :foreign_type_key,
214
+ :polymorphic_key, # same as :association_foreign_key
215
+ :polymorphic_type_key,
216
+ :dependent, # default :destroy, only affects the join table
217
+ :skip_duplicates, # default true, only affects the polymorphic collection
218
+ :ignore_duplicates, # deprecated
219
+ :is_double,
220
+ :rename_individual_collections,
221
+ :reverse_association_id, # not used
222
+ :singular_reverse_association_id,
223
+ :conflicts,
224
+ :extend,
225
+ :join_class_name,
226
+ :join_extend,
227
+ :parent_extend,
228
+ :table_aliases,
229
+ :select, # applies to the polymorphic relationship
230
+ :conditions, # applies to the polymorphic relationship, the children, and the join
231
+ # :include,
232
+ :parent_conditions,
233
+ :parent_order,
234
+ :order, # applies to the polymorphic relationship, the children, and the join
235
+ :group, # only applies to the polymorphic relationship and the children
236
+ :limit, # only applies to the polymorphic relationship and the children
237
+ :offset, # only applies to the polymorphic relationship
238
+ :parent_order,
239
+ :parent_group,
240
+ :parent_limit,
241
+ :parent_offset,
242
+ # :source,
243
+ :namespace,
244
+ :uniq, # XXX untested, only applies to the polymorphic relationship
245
+ # :finder_sql,
246
+ # :counter_sql,
247
+ # :before_add,
248
+ # :after_add,
249
+ # :before_remove,
250
+ # :after_remove
251
+ :dummy
252
+ )
253
+
254
+ # validate against the most frequent configuration mistakes
255
+ verify_pluralization_of(association_id)
256
+ raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array
257
+
258
+ # if an association with this name is already defined, we recreate it with
259
+ # the new and old :from-options combined
260
+ if self.reflections[association_id]
261
+ options[:from] += self.reflections[association_id].options[:from]
262
+ options[:from].uniq!
263
+ end
264
+ options[:from].each { |plural| verify_pluralization_of(plural) }
265
+
266
+ options[:as] ||= self.name.demodulize.underscore.to_sym
267
+ options[:conflicts] = Array(options[:conflicts])
268
+ options[:foreign_key] ||= "#{options[:as]}_id"
269
+
270
+ options[:association_foreign_key] =
271
+ options[:polymorphic_key] ||= "#{association_id._singularize}_id"
272
+ options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
273
+
274
+ if options.has_key? :ignore_duplicates
275
+ _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
276
+ options[:skip_duplicates] = options[:ignore_duplicates]
277
+ end
278
+ options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
279
+ options[:dependent] = :destroy unless options.has_key? :dependent
280
+ options[:conditions] = sanitize_sql(options[:conditions])
281
+
282
+ # options[:finder_sql] ||= "(options[:polymorphic_key]
283
+
284
+ options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
285
+
286
+ # set up namespaces if we have a namespace key
287
+ # XXX needs test coverage
288
+ if options[:namespace]
289
+ namespace = options[:namespace].to_s.chomp("/") + "/"
290
+ options[:from].map! do |child|
291
+ "#{namespace}#{child}".to_sym
292
+ end
293
+ options[:through] = "#{namespace}#{options[:through]}".to_sym
294
+ end
295
+
296
+ options[:join_class_name] ||= options[:through]._classify
297
+ options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
298
+ options[:select] ||= build_select(association_id, options[:table_aliases])
299
+
300
+ options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
301
+ options[:through] = demodulate(options[:through]).to_sym
302
+
303
+ options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
304
+ options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
305
+ options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")
306
+
307
+ # create the reflection object
308
+ create_reflection(:has_many_polymorphs, association_id, options, self).tap do |reflection|
309
+ # set up the other related associations
310
+ create_join_association(association_id, reflection)
311
+ create_has_many_through_associations_for_parent_to_children(association_id, reflection)
312
+ create_has_many_through_associations_for_children_to_parent(association_id, reflection)
313
+ end
314
+ end
315
+
316
+ private
317
+
318
+ def extract_double_collections(options)
319
+ collections = options._select do |key, value|
320
+ value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
321
+ end
322
+
323
+ 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
324
+
325
+ options = options._select do |key, value|
326
+ !collections[key]
327
+ end
328
+
329
+ [collections, options]
330
+ end
331
+
332
+ def make_general_option_keys_specific!(options, collections)
333
+ collection_option_keys = Hash[*collections.keys.map do |key|
334
+ [key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}]
335
+ end._flatten_once]
336
+
337
+ collections.keys.each do |collection|
338
+ options.each do |key, value|
339
+ next if collection_option_keys.values.flatten.include? key
340
+ # shift the general options to the individual sides
341
+ collection_key = "#{collection}_#{key}".to_sym
342
+ collection_value = options[collection_key]
343
+ case key
344
+ when :conditions
345
+ collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
346
+ options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
347
+ when :order
348
+ options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
349
+ when :extend, :join_extend
350
+ options[collection_key] = Array(collection_value) + Array(value)
351
+ else
352
+ options[collection_key] ||= value
353
+ end
354
+ end
355
+ end
356
+
357
+ collection_option_keys
358
+ end
359
+
360
+ # table mapping for use at the instantiation point
361
+
362
+ def build_table_aliases(from)
363
+ # for the targets
364
+ {}.tap do |aliases|
365
+ from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
366
+ begin
367
+ table = plural._as_class.table_name
368
+ rescue NameError => e
369
+ raise PolymorphicError, "Could not find a valid class for #{plural.inspect} (tried #{plural.to_s._classify}). If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead."
370
+ end
371
+ begin
372
+ plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
373
+ aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
374
+ end
375
+ rescue ActiveRecord::StatementInvalid => e
376
+ _logger_warn "Looks like your table doesn't exist for #{plural.to_s._classify}.\nError #{e}\nSkipping..."
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+ def build_select(association_id, aliases)
383
+ # <tt>instantiate</tt> has to know which reflection the results are coming from
384
+ (["\'#{self.name}\' AS polymorphic_parent_class",
385
+ "\'#{association_id}\' AS polymorphic_association_id"] +
386
+ aliases.map do |table, _alias|
387
+ "#{table} AS #{_alias}"
388
+ end.sort).join(", ")
389
+ end
390
+
391
+ # method sub-builders
392
+
393
+ def create_join_association(association_id, reflection)
394
+
395
+ options = {
396
+ :foreign_key => reflection.options[:foreign_key],
397
+ :dependent => reflection.options[:dependent],
398
+ :class_name => reflection.klass.name,
399
+ :extend => reflection.options[:join_extend]
400
+ # :limit => reflection.options[:limit],
401
+ # :offset => reflection.options[:offset],
402
+ # :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
403
+ # :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
404
+ }
405
+
406
+ if reflection.options[:foreign_type_key]
407
+ type_check = "#{reflection.options[:join_class_name].constantize.quoted_table_name}.#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
408
+ conjunction = options[:conditions] ? " AND " : nil
409
+ options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
410
+ options[:as] = reflection.options[:as]
411
+ end
412
+
413
+ has_many(reflection.options[:through], options)
414
+
415
+ inject_before_save_into_join_table(association_id, reflection)
416
+ end
417
+
418
+ def inject_before_save_into_join_table(association_id, reflection)
419
+ sti_hook = "sti_class_rewrite"
420
+ polymorphic_type_key = reflection.options[:polymorphic_type_key]
421
+
422
+ reflection.klass.class_eval %{
423
+ unless [self._save_callbacks.map(&:raw_filter)].flatten.include?(:#{sti_hook})
424
+ before_save :#{sti_hook}
425
+
426
+ def #{sti_hook}
427
+ self.send(:#{polymorphic_type_key}=, self.#{polymorphic_type_key}.constantize.base_class.name)
428
+ end
429
+ end
430
+ }
431
+ end
432
+
433
+ def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
434
+
435
+ child_pluralization_map(association_id, reflection).each do |plural, singular|
436
+ if singular == reflection.options[:as]
437
+ raise PolymorphicError, if reflection.options[:is_double]
438
+ "You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
439
+ else
440
+ "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
441
+ end
442
+ end
443
+
444
+ parent = self
445
+ plural._as_class.instance_eval do
446
+ # this shouldn't be called at all during doubles; there is
447
+ # no way to traverse to a double polymorphic parent
448
+ # (TODO is that right?)
449
+ unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym
450
+
451
+ # the join table
452
+ through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym
453
+ has_many(through,
454
+ :as => association_id._singularize,
455
+ # :source => association_id._singularize,
456
+ # :source_type => reflection.options[:polymorphic_type_key],
457
+ :class_name => reflection.klass.name,
458
+ :dependent => reflection.options[:dependent],
459
+ :extend => reflection.options[:join_extend],
460
+ # :limit => reflection.options[:limit],
461
+ # :offset => reflection.options[:offset],
462
+ :order => devolve(association_id, reflection, reflection.options[:parent_order], reflection.klass),
463
+ :conditions => devolve(association_id, reflection, reflection.options[:parent_conditions], reflection.klass)
464
+ )
465
+
466
+ # the association to the target's parents
467
+ association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym
468
+ has_many(association,
469
+ :through => through,
470
+ :class_name => parent.name,
471
+ :source => reflection.options[:as],
472
+ :foreign_key => reflection.options[:foreign_key],
473
+ :extend => reflection.options[:parent_extend],
474
+ :conditions => reflection.options[:parent_conditions],
475
+ :order => reflection.options[:parent_order],
476
+ :offset => reflection.options[:parent_offset],
477
+ :limit => reflection.options[:parent_limit],
478
+ :group => reflection.options[:parent_group]
479
+ )
480
+
481
+ # debugger if association == :parents
482
+ # nil
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
489
+ child_pluralization_map(association_id, reflection).each do |plural, singular|
490
+ current_association = demodulate(child_association_map(association_id, reflection)[plural])
491
+ source = demodulate(singular)
492
+
493
+ if reflection.options[:conflicts].include? plural
494
+ # XXX check this
495
+ current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
496
+ source = "#{source}_as_#{association_id._singularize}".to_sym
497
+ end
498
+
499
+ # make push/delete accessible from the individual collections but still operate via the general collection
500
+ extension_module = self.class_eval %[
501
+ module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
502
+ def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end
503
+ alias :<< :push
504
+ def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
505
+ def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
506
+ self
507
+ end
508
+ ]
509
+
510
+ has_many(
511
+ current_association.to_sym,
512
+ :through => reflection.options[:through],
513
+ :source => association_id._singularize,
514
+ :source_type => plural._as_class.base_class.name,
515
+ :class_name => plural._as_class.name, # make STI not conflate subtypes
516
+ :extend => (Array(extension_module) + reflection.options[:extend]),
517
+ :limit => reflection.options[:limit],
518
+ # :offset => reflection.options[:offset],
519
+ :order => devolve(association_id, reflection, reflection.options[:order], plural._as_class),
520
+ :conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class),
521
+ :group => devolve(association_id, reflection, reflection.options[:group], plural._as_class)
522
+ )
523
+ end
524
+ end
525
+
526
+ # some support methods
527
+
528
+ def child_pluralization_map(association_id, reflection)
529
+ Hash[*reflection.options[:from].map do |plural|
530
+ [plural, plural._singularize]
531
+ end.flatten]
532
+ end
533
+
534
+ def child_association_map(association_id, reflection)
535
+ Hash[*reflection.options[:from].map do |plural|
536
+ [plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
537
+ end.flatten]
538
+ end
539
+
540
+ def demodulate(s)
541
+ s.to_s.gsub('/', '_').to_sym
542
+ end
543
+
544
+ def build_join_table_symbol(association_id, name)
545
+ [name.to_s, association_id.to_s].sort.join("_").to_sym
546
+ end
547
+
548
+ def all_classes_for(association_id, reflection)
549
+ klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
550
+ klasses += klasses.map(&:base_class)
551
+ klasses.uniq
552
+ end
553
+
554
+ def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false)
555
+ # XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
556
+ return unless string
557
+ string = string.dup
558
+ # _logger_debug "devolving #{string} for #{klass}"
559
+ inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
560
+ [klass, klass.base_class, reflection.klass, reflection.klass.base_class])
561
+ inappropriate_classes.map do |klass|
562
+ klass.columns.map do |column|
563
+ [klass.table_name, column.name]
564
+ end.map do |table, column|
565
+ ["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
566
+ end
567
+ end.flatten.sort_by(&:size).reverse.each do |quoted_reference|
568
+ # _logger_debug "devolved #{quoted_reference} to NULL"
569
+ # XXX clause removal would go here
570
+ string.gsub!(quoted_reference, "NULL")
571
+ end
572
+ # _logger_debug "altered to #{string}"
573
+ string
574
+ end
575
+
576
+ def verify_pluralization_of(sym)
577
+ sym = sym.to_s
578
+ singular = sym.singularize
579
+ plural = singular.pluralize
580
+ 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
581
+ end
582
+
583
+ def spiked_create_extension_module(association_id, extensions, identifier = nil)
584
+ module_extensions = extensions.select{|e| e.is_a? Module}
585
+ proc_extensions = extensions.select{|e| e.is_a? Proc }
586
+
587
+ # support namespaced anonymous blocks as well as multiple procs
588
+ proc_extensions.each_with_index do |proc_extension, index|
589
+ module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
590
+ the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
591
+ the_module.class_eval &proc_extension
592
+ module_extensions << the_module
593
+ end
594
+ module_extensions
595
+ end
596
+ end
597
+ end
598
+ end