has_many_polymorphs 2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (177) hide show
  1. data/CHANGELOG +86 -0
  2. data/LICENSE +184 -0
  3. data/Manifest +173 -0
  4. data/README +205 -0
  5. data/Rakefile +28 -0
  6. data/TODO +2 -0
  7. data/examples/hmph.rb +69 -0
  8. data/generators/tagging/tagging_generator.rb +97 -0
  9. data/generators/tagging/templates/migration.rb +28 -0
  10. data/generators/tagging/templates/tag.rb +39 -0
  11. data/generators/tagging/templates/tag_test.rb +15 -0
  12. data/generators/tagging/templates/tagging.rb +16 -0
  13. data/generators/tagging/templates/tagging_extensions.rb +203 -0
  14. data/generators/tagging/templates/tagging_test.rb +85 -0
  15. data/generators/tagging/templates/taggings.yml +23 -0
  16. data/generators/tagging/templates/tags.yml +7 -0
  17. data/has_many_polymorphs.gemspec +36 -0
  18. data/init.rb +2 -0
  19. data/lib/has_many_polymorphs/association.rb +160 -0
  20. data/lib/has_many_polymorphs/autoload.rb +69 -0
  21. data/lib/has_many_polymorphs/base.rb +60 -0
  22. data/lib/has_many_polymorphs/class_methods.rb +600 -0
  23. data/lib/has_many_polymorphs/configuration.rb +19 -0
  24. data/lib/has_many_polymorphs/debugging_tools.rb +103 -0
  25. data/lib/has_many_polymorphs/rake_task_redefine_task.rb +35 -0
  26. data/lib/has_many_polymorphs/reflection.rb +58 -0
  27. data/lib/has_many_polymorphs/support_methods.rb +88 -0
  28. data/lib/has_many_polymorphs.rb +27 -0
  29. data/test/fixtures/bow_wows.yml +10 -0
  30. data/test/fixtures/cats.yml +18 -0
  31. data/test/fixtures/eaters_foodstuffs.yml +0 -0
  32. data/test/fixtures/fish.yml +12 -0
  33. data/test/fixtures/frogs.yml +5 -0
  34. data/test/fixtures/keep_your_enemies_close.yml +0 -0
  35. data/test/fixtures/little_whale_pupils.yml +0 -0
  36. data/test/fixtures/people.yml +7 -0
  37. data/test/fixtures/petfoods.yml +11 -0
  38. data/test/fixtures/whales.yml +5 -0
  39. data/test/fixtures/wild_boars.yml +10 -0
  40. data/test/generator/tagging_generator_test.rb +42 -0
  41. data/test/integration/app/README +182 -0
  42. data/test/integration/app/Rakefile +19 -0
  43. data/test/integration/app/app/controllers/application.rb +7 -0
  44. data/test/integration/app/app/controllers/bones_controller.rb +5 -0
  45. data/test/integration/app/app/helpers/addresses_helper.rb +2 -0
  46. data/test/integration/app/app/helpers/application_helper.rb +3 -0
  47. data/test/integration/app/app/helpers/bones_helper.rb +2 -0
  48. data/test/integration/app/app/helpers/sellers_helper.rb +28 -0
  49. data/test/integration/app/app/helpers/states_helper.rb +2 -0
  50. data/test/integration/app/app/helpers/users_helper.rb +2 -0
  51. data/test/integration/app/app/models/bone.rb +2 -0
  52. data/test/integration/app/app/models/double_sti_parent.rb +2 -0
  53. data/test/integration/app/app/models/double_sti_parent_relationship.rb +2 -0
  54. data/test/integration/app/app/models/organic_substance.rb +2 -0
  55. data/test/integration/app/app/models/single_sti_parent.rb +4 -0
  56. data/test/integration/app/app/models/single_sti_parent_relationship.rb +4 -0
  57. data/test/integration/app/app/models/stick.rb +2 -0
  58. data/test/integration/app/app/models/stone.rb +2 -0
  59. data/test/integration/app/app/views/addresses/edit.html.erb +12 -0
  60. data/test/integration/app/app/views/addresses/index.html.erb +18 -0
  61. data/test/integration/app/app/views/addresses/new.html.erb +11 -0
  62. data/test/integration/app/app/views/addresses/show.html.erb +3 -0
  63. data/test/integration/app/app/views/bones/index.rhtml +5 -0
  64. data/test/integration/app/app/views/layouts/addresses.html.erb +17 -0
  65. data/test/integration/app/app/views/layouts/sellers.html.erb +17 -0
  66. data/test/integration/app/app/views/layouts/states.html.erb +17 -0
  67. data/test/integration/app/app/views/layouts/users.html.erb +17 -0
  68. data/test/integration/app/app/views/sellers/edit.html.erb +12 -0
  69. data/test/integration/app/app/views/sellers/index.html.erb +20 -0
  70. data/test/integration/app/app/views/sellers/new.html.erb +11 -0
  71. data/test/integration/app/app/views/sellers/show.html.erb +3 -0
  72. data/test/integration/app/app/views/states/edit.html.erb +12 -0
  73. data/test/integration/app/app/views/states/index.html.erb +19 -0
  74. data/test/integration/app/app/views/states/new.html.erb +11 -0
  75. data/test/integration/app/app/views/states/show.html.erb +3 -0
  76. data/test/integration/app/app/views/users/edit.html.erb +12 -0
  77. data/test/integration/app/app/views/users/index.html.erb +22 -0
  78. data/test/integration/app/app/views/users/new.html.erb +11 -0
  79. data/test/integration/app/app/views/users/show.html.erb +3 -0
  80. data/test/integration/app/config/boot.rb +110 -0
  81. data/test/integration/app/config/database.yml +17 -0
  82. data/test/integration/app/config/environment.rb +19 -0
  83. data/test/integration/app/config/environment.rb.canonical +19 -0
  84. data/test/integration/app/config/environments/development.rb +9 -0
  85. data/test/integration/app/config/environments/production.rb +18 -0
  86. data/test/integration/app/config/environments/test.rb +19 -0
  87. data/test/integration/app/config/locomotive.yml +6 -0
  88. data/test/integration/app/config/routes.rb +33 -0
  89. data/test/integration/app/config/ultrasphinx/default.base +56 -0
  90. data/test/integration/app/config/ultrasphinx/development.conf.canonical +155 -0
  91. data/test/integration/app/db/migrate/001_create_sticks.rb +11 -0
  92. data/test/integration/app/db/migrate/002_create_stones.rb +11 -0
  93. data/test/integration/app/db/migrate/003_create_organic_substances.rb +11 -0
  94. data/test/integration/app/db/migrate/004_create_bones.rb +8 -0
  95. data/test/integration/app/db/migrate/005_create_single_sti_parents.rb +11 -0
  96. data/test/integration/app/db/migrate/006_create_double_sti_parents.rb +11 -0
  97. data/test/integration/app/db/migrate/007_create_single_sti_parent_relationships.rb +13 -0
  98. data/test/integration/app/db/migrate/008_create_double_sti_parent_relationships.rb +14 -0
  99. data/test/integration/app/db/migrate/009_create_library_model.rb +11 -0
  100. data/test/integration/app/doc/README_FOR_APP +2 -0
  101. data/test/integration/app/generators/commenting_generator_test.rb +83 -0
  102. data/test/integration/app/lib/library_model.rb +2 -0
  103. data/test/integration/app/public/404.html +30 -0
  104. data/test/integration/app/public/500.html +30 -0
  105. data/test/integration/app/public/dispatch.cgi +10 -0
  106. data/test/integration/app/public/dispatch.fcgi +24 -0
  107. data/test/integration/app/public/dispatch.rb +10 -0
  108. data/test/integration/app/public/favicon.ico +0 -0
  109. data/test/integration/app/public/images/rails.png +0 -0
  110. data/test/integration/app/public/index.html +277 -0
  111. data/test/integration/app/public/javascripts/application.js +2 -0
  112. data/test/integration/app/public/javascripts/controls.js +833 -0
  113. data/test/integration/app/public/javascripts/dragdrop.js +942 -0
  114. data/test/integration/app/public/javascripts/effects.js +1088 -0
  115. data/test/integration/app/public/javascripts/prototype.js +2515 -0
  116. data/test/integration/app/public/robots.txt +1 -0
  117. data/test/integration/app/public/stylesheets/scaffold.css +74 -0
  118. data/test/integration/app/script/about +3 -0
  119. data/test/integration/app/script/breakpointer +3 -0
  120. data/test/integration/app/script/console +3 -0
  121. data/test/integration/app/script/destroy +3 -0
  122. data/test/integration/app/script/generate +3 -0
  123. data/test/integration/app/script/performance/benchmarker +3 -0
  124. data/test/integration/app/script/performance/profiler +3 -0
  125. data/test/integration/app/script/plugin +3 -0
  126. data/test/integration/app/script/process/inspector +3 -0
  127. data/test/integration/app/script/process/reaper +3 -0
  128. data/test/integration/app/script/process/spawner +3 -0
  129. data/test/integration/app/script/runner +3 -0
  130. data/test/integration/app/script/server +3 -0
  131. data/test/integration/app/test/fixtures/double_sti_parent_relationships.yml +7 -0
  132. data/test/integration/app/test/fixtures/double_sti_parents.yml +7 -0
  133. data/test/integration/app/test/fixtures/organic_substances.yml +5 -0
  134. data/test/integration/app/test/fixtures/single_sti_parent_relationships.yml +7 -0
  135. data/test/integration/app/test/fixtures/single_sti_parents.yml +7 -0
  136. data/test/integration/app/test/fixtures/sticks.yml +7 -0
  137. data/test/integration/app/test/fixtures/stones.yml +7 -0
  138. data/test/integration/app/test/functional/addresses_controller_test.rb +57 -0
  139. data/test/integration/app/test/functional/bones_controller_test.rb +8 -0
  140. data/test/integration/app/test/functional/sellers_controller_test.rb +57 -0
  141. data/test/integration/app/test/functional/states_controller_test.rb +57 -0
  142. data/test/integration/app/test/functional/users_controller_test.rb +57 -0
  143. data/test/integration/app/test/test_helper.rb +8 -0
  144. data/test/integration/app/test/unit/bone_test.rb +8 -0
  145. data/test/integration/app/test/unit/double_sti_parent_relationship_test.rb +8 -0
  146. data/test/integration/app/test/unit/double_sti_parent_test.rb +8 -0
  147. data/test/integration/app/test/unit/organic_substance_test.rb +8 -0
  148. data/test/integration/app/test/unit/single_sti_parent_relationship_test.rb +8 -0
  149. data/test/integration/app/test/unit/single_sti_parent_test.rb +8 -0
  150. data/test/integration/app/test/unit/stick_test.rb +8 -0
  151. data/test/integration/app/test/unit/stone_test.rb +8 -0
  152. data/test/integration/server_test.rb +43 -0
  153. data/test/models/aquatic/fish.rb +5 -0
  154. data/test/models/aquatic/pupils_whale.rb +7 -0
  155. data/test/models/aquatic/whale.rb +15 -0
  156. data/test/models/beautiful_fight_relationship.rb +26 -0
  157. data/test/models/canine.rb +9 -0
  158. data/test/models/cat.rb +5 -0
  159. data/test/models/dog.rb +18 -0
  160. data/test/models/eaters_foodstuff.rb +8 -0
  161. data/test/models/frog.rb +4 -0
  162. data/test/models/kitten.rb +3 -0
  163. data/test/models/parentship.rb +4 -0
  164. data/test/models/person.rb +9 -0
  165. data/test/models/petfood.rb +39 -0
  166. data/test/models/tabby.rb +2 -0
  167. data/test/models/wild_boar.rb +3 -0
  168. data/test/modules/extension_module.rb +9 -0
  169. data/test/modules/other_extension_module.rb +9 -0
  170. data/test/patches/symlinked_plugins_1.2.6.diff +46 -0
  171. data/test/schema.rb +87 -0
  172. data/test/setup.rb +14 -0
  173. data/test/test_helper.rb +52 -0
  174. data/test/unit/has_many_polymorphs_test.rb +713 -0
  175. data.tar.gz.sig +1 -0
  176. metadata +279 -0
  177. metadata.gz.sig +0 -0
@@ -0,0 +1,600 @@
1
+
2
+ module ActiveRecord #:nodoc:
3
+ module Associations #:nodoc:
4
+
5
+ =begin rdoc
6
+
7
+ Class methods added to ActiveRecord::Base for setting up polymorphic associations.
8
+
9
+ == Notes
10
+
11
+ 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>.
12
+
13
+ Namespaced models follow the Rails <tt>underscore</tt> convention. ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
14
+
15
+ 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.
16
+
17
+ There is a tentative report that you can make the parent model be its own join model, but this is untested.
18
+
19
+ =end
20
+
21
+ module PolymorphicClassMethods
22
+
23
+ RESERVED_DOUBLES_KEYS = [:conditions, :order, :limit, :offset, :extend, :skip_duplicates,
24
+ :join_extend, :dependent, :rename_individual_collections,
25
+ :namespace] #:nodoc:
26
+
27
+ =begin rdoc
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 <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.
42
+
43
+ The two association names and their value arrays are the only required parameters.
44
+
45
+ == Available options
46
+
47
+ 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>.
48
+
49
+ <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>.
50
+ <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>.
51
+ <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>.
52
+ <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
53
+ <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
54
+ <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
55
+ <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
56
+ <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
57
+ <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
58
+ <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.
59
+
60
+ =end
61
+
62
+ def acts_as_double_polymorphic_join options={}, &extension
63
+
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
+ raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key
83
+ end
84
+
85
+ parents = collections[parent_hash_key]
86
+ conflicts = (children & parents) # set intersection
87
+ parents.each do |plural_parent_name|
88
+
89
+ parent_class = plural_parent_name._as_class
90
+ singular_reverse_association_id = parent_hash_key._singularize
91
+
92
+ internal_options = {
93
+ :is_double => true,
94
+ :from => children,
95
+ :as => singular_reverse_association_id,
96
+ :through => join_name.to_sym,
97
+ :foreign_key => parent_foreign_key,
98
+ :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
99
+ :singular_reverse_association_id => singular_reverse_association_id,
100
+ :conflicts => conflicts
101
+ }
102
+
103
+ general_options = Hash[*options._select do |key, value|
104
+ collection_option_keys[association_id].include? key and !value.nil?
105
+ end.map do |key, value|
106
+ [key.to_s[association_id.to_s.length+1..-1].to_sym, value]
107
+ end._flatten_once] # rename side-specific options to general names
108
+
109
+ general_options.each do |key, value|
110
+ # avoid clobbering keys that appear in both option sets
111
+ if internal_options[key]
112
+ general_options[key] = Array(value) + Array(internal_options[key])
113
+ end
114
+ end
115
+
116
+ parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
117
+
118
+ if conflicts.include? plural_parent_name
119
+ # unify the alternate sides of the conflicting children
120
+ (conflicts).each do |method_name|
121
+ unless parent_class.instance_methods.include?(method_name)
122
+ parent_class.send(:define_method, method_name) do
123
+ (self.send("#{singular_reverse_association_id}_#{method_name}") +
124
+ self.send("#{association_id._singularize}_#{method_name}")).freeze
125
+ end
126
+ end
127
+ end
128
+
129
+ # unify the join model... join model is always renamed for doubles, unlike child associations
130
+ unless parent_class.instance_methods.include?(join_name)
131
+ parent_class.send(:define_method, join_name) do
132
+ (self.send("#{join_name}_as_#{singular_reverse_association_id}") +
133
+ self.send("#{join_name}_as_#{association_id._singularize}")).freeze
134
+ end
135
+ end
136
+ else
137
+ unless parent_class.instance_methods.include?(join_name)
138
+ parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}")
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def extract_double_collections(options)
149
+ collections = options._select do |key, value|
150
+ value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
151
+ end
152
+
153
+ 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
154
+
155
+ options = options._select do |key, value|
156
+ !collections[key]
157
+ end
158
+
159
+ [collections, options]
160
+ end
161
+
162
+ def make_general_option_keys_specific!(options, collections)
163
+ collection_option_keys = Hash[*collections.keys.map do |key|
164
+ [key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}]
165
+ end._flatten_once]
166
+
167
+ collections.keys.each do |collection|
168
+ options.each do |key, value|
169
+ next if collection_option_keys.values.flatten.include? key
170
+ # shift the general options to the individual sides
171
+ collection_key = "#{collection}_#{key}".to_sym
172
+ collection_value = options[collection_key]
173
+ case key
174
+ when :conditions
175
+ collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
176
+ options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
177
+ when :order
178
+ options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
179
+ when :extend, :join_extend
180
+ options[collection_key] = Array(collection_value) + Array(value)
181
+ else
182
+ options[collection_key] ||= value
183
+ end
184
+ end
185
+ end
186
+
187
+ collection_option_keys
188
+ end
189
+
190
+
191
+
192
+ public
193
+
194
+ =begin rdoc
195
+
196
+ This method createds a single-sided polymorphic relationship.
197
+
198
+ class Petfood < ActiveRecord::Base
199
+ has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
200
+ end
201
+
202
+ The only required parameter, aside from the association name, is <tt>:from</tt>.
203
+
204
+ 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.)
205
+
206
+ == Available options
207
+
208
+ <tt>:from</tt>:: An array of symbols representing the target models. Required.
209
+ <tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
210
+ <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).
211
+ <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>.
212
+ <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>.
213
+ <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.
214
+ <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
215
+ <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
216
+ <tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
217
+ <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
218
+ <tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
219
+ <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
220
+ <tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
221
+ <tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
222
+ <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
223
+ <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
224
+ <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.
225
+ <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.
226
+ <tt>:foreign_key</tt>:: The column name for the parent's id in the join.
227
+ <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.
228
+ <tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
229
+ <tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
230
+
231
+ If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>.
232
+
233
+ == On condition nullification
234
+
235
+ 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>.
236
+
237
+ 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.
238
+
239
+ =end
240
+
241
+ def has_many_polymorphs (association_id, options = {}, &extension)
242
+ _logger_debug "associating #{self}.#{association_id}"
243
+ reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
244
+ # puts "Created reflection #{reflection.inspect}"
245
+ # configure_dependency_for_has_many(reflection)
246
+ collection_reader_method(reflection, PolymorphicAssociation)
247
+ end
248
+
249
+ # Composed method that assigns option defaults, builds the reflection object, and sets up all the related associations on the parent, join, and targets.
250
+ def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
251
+ options.assert_valid_keys(
252
+ :from,
253
+ :as,
254
+ :through,
255
+ :foreign_key,
256
+ :foreign_type_key,
257
+ :polymorphic_key, # same as :association_foreign_key
258
+ :polymorphic_type_key,
259
+ :dependent, # default :destroy, only affects the join table
260
+ :skip_duplicates, # default true, only affects the polymorphic collection
261
+ :ignore_duplicates, # deprecated
262
+ :is_double,
263
+ :rename_individual_collections,
264
+ :reverse_association_id, # not used
265
+ :singular_reverse_association_id,
266
+ :conflicts,
267
+ :extend,
268
+ :join_class_name,
269
+ :join_extend,
270
+ :parent_extend,
271
+ :table_aliases,
272
+ :select, # applies to the polymorphic relationship
273
+ :conditions, # applies to the polymorphic relationship, the children, and the join
274
+ # :include,
275
+ :parent_conditions,
276
+ :parent_order,
277
+ :order, # applies to the polymorphic relationship, the children, and the join
278
+ :group, # only applies to the polymorphic relationship and the children
279
+ :limit, # only applies to the polymorphic relationship and the children
280
+ :offset, # only applies to the polymorphic relationship
281
+ :parent_order,
282
+ :parent_group,
283
+ :parent_limit,
284
+ :parent_offset,
285
+ # :source,
286
+ :namespace,
287
+ :uniq, # XXX untested, only applies to the polymorphic relationship
288
+ # :finder_sql,
289
+ # :counter_sql,
290
+ # :before_add,
291
+ # :after_add,
292
+ # :before_remove,
293
+ # :after_remove
294
+ :dummy)
295
+
296
+ # validate against the most frequent configuration mistakes
297
+ verify_pluralization_of(association_id)
298
+ raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array
299
+ options[:from].each{|plural| verify_pluralization_of(plural)}
300
+
301
+ options[:as] ||= self.name.demodulize.underscore.to_sym
302
+ options[:conflicts] = Array(options[:conflicts])
303
+ options[:foreign_key] ||= "#{options[:as]}_id"
304
+
305
+ options[:association_foreign_key] =
306
+ options[:polymorphic_key] ||= "#{association_id._singularize}_id"
307
+ options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
308
+
309
+ if options.has_key? :ignore_duplicates
310
+ _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
311
+ options[:skip_duplicates] = options[:ignore_duplicates]
312
+ end
313
+ options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
314
+ options[:dependent] = :destroy unless options.has_key? :dependent
315
+ options[:conditions] = sanitize_sql(options[:conditions])
316
+
317
+ # options[:finder_sql] ||= "(options[:polymorphic_key]
318
+
319
+ options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
320
+
321
+ # set up namespaces if we have a namespace key
322
+ # XXX needs test coverage
323
+ if options[:namespace]
324
+ namespace = options[:namespace].to_s.chomp("/") + "/"
325
+ options[:from].map! do |child|
326
+ "#{namespace}#{child}".to_sym
327
+ end
328
+ options[:through] = "#{namespace}#{options[:through]}".to_sym
329
+ end
330
+
331
+ options[:join_class_name] ||= options[:through]._classify
332
+ options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
333
+ options[:select] ||= build_select(association_id, options[:table_aliases])
334
+
335
+ options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
336
+ options[:through] = demodulate(options[:through]).to_sym
337
+
338
+ options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
339
+ options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
340
+ options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")
341
+
342
+ # create the reflection object
343
+ returning(create_reflection(:has_many_polymorphs, association_id, options, self)) do |reflection|
344
+ # set up the other related associations
345
+ create_join_association(association_id, reflection)
346
+ create_has_many_through_associations_for_parent_to_children(association_id, reflection)
347
+ create_has_many_through_associations_for_children_to_parent(association_id, reflection)
348
+ end
349
+ end
350
+
351
+ private
352
+
353
+
354
+ # table mapping for use at the instantiation point
355
+
356
+ def build_table_aliases(from)
357
+ # for the targets
358
+ returning({}) do |aliases|
359
+ from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
360
+ begin
361
+ table = plural._as_class.table_name
362
+ rescue NameError => e
363
+ 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."
364
+ end
365
+ begin
366
+ plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
367
+ aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
368
+ end
369
+ rescue ActiveRecord::StatementInvalid => e
370
+ _logger_warn "Looks like your table doesn't exist for #{plural.to_s._classify}.\nError #{e}\nSkipping..."
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ def build_select(association_id, aliases)
377
+ # <tt>instantiate</tt> has to know which reflection the results are coming from
378
+ (["\'#{self.name}\' AS polymorphic_parent_class",
379
+ "\'#{association_id}\' AS polymorphic_association_id"] +
380
+ aliases.map do |table, _alias|
381
+ "#{table} AS #{_alias}"
382
+ end.sort).join(", ")
383
+ end
384
+
385
+ # method sub-builders
386
+
387
+ def create_join_association(association_id, reflection)
388
+
389
+ options = {
390
+ :foreign_key => reflection.options[:foreign_key],
391
+ :dependent => reflection.options[:dependent],
392
+ :class_name => reflection.klass.name,
393
+ :extend => reflection.options[:join_extend]
394
+ # :limit => reflection.options[:limit],
395
+ # :offset => reflection.options[:offset],
396
+ # :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
397
+ # :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
398
+ }
399
+
400
+ if reflection.options[:foreign_type_key]
401
+ type_check = "#{reflection.options[:join_class_name].constantize.quoted_table_name}.#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
402
+ conjunction = options[:conditions] ? " AND " : nil
403
+ options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
404
+ options[:as] = reflection.options[:as]
405
+ end
406
+
407
+ has_many(reflection.options[:through], options)
408
+
409
+ inject_before_save_into_join_table(association_id, reflection)
410
+ end
411
+
412
+ def inject_before_save_into_join_table(association_id, reflection)
413
+ sti_hook = "sti_class_rewrite"
414
+ rewrite_procedure = %[self.send(:#{reflection.options[:polymorphic_type_key]}=, self.#{reflection.options[:polymorphic_type_key]}.constantize.base_class.name)]
415
+
416
+ # XXX should be abstracted?
417
+ reflection.klass.class_eval %[
418
+ unless instance_methods.include? "before_save_with_#{sti_hook}"
419
+ if instance_methods.include? "before_save"
420
+ alias_method :before_save_without_#{sti_hook}, :before_save
421
+ def before_save_with_#{sti_hook}
422
+ before_save_without_#{sti_hook}
423
+ #{rewrite_procedure}
424
+ end
425
+ else
426
+ def before_save_with_#{sti_hook}
427
+ #{rewrite_procedure}
428
+ end
429
+ end
430
+ alias_method :before_save, :before_save_with_#{sti_hook}
431
+ end
432
+ ]
433
+ end
434
+
435
+ def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
436
+
437
+ child_pluralization_map(association_id, reflection).each do |plural, singular|
438
+ if singular == reflection.options[:as]
439
+ raise PolymorphicError, if reflection.options[:is_double]
440
+ "You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
441
+ else
442
+ "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
443
+ end
444
+ end
445
+
446
+ parent = self
447
+ plural._as_class.instance_eval do
448
+ # this shouldn't be called at all during doubles; there is no way to traverse to a double polymorphic parent (XXX 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
+ # debugger if association == :parents
481
+ #
482
+ # nil
483
+
484
+ end
485
+ end
486
+ end
487
+ end
488
+
489
+ def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
490
+ child_pluralization_map(association_id, reflection).each do |plural, singular|
491
+ #puts ":source => #{child}"
492
+ current_association = demodulate(child_association_map(association_id, reflection)[plural])
493
+ source = demodulate(singular)
494
+
495
+ if reflection.options[:conflicts].include? plural
496
+ # XXX check this
497
+ current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
498
+ source = "#{source}_as_#{association_id._singularize}".to_sym
499
+ end
500
+
501
+ # make push/delete accessible from the individual collections but still operate via the general collection
502
+ extension_module = self.class_eval %[
503
+ module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
504
+ def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end
505
+ alias :<< :push
506
+ def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
507
+ def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
508
+ self
509
+ end]
510
+
511
+ has_many(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
+
524
+ end
525
+ end
526
+
527
+ # some support methods
528
+
529
+ def child_pluralization_map(association_id, reflection)
530
+ Hash[*reflection.options[:from].map do |plural|
531
+ [plural, plural._singularize]
532
+ end.flatten]
533
+ end
534
+
535
+ def child_association_map(association_id, reflection)
536
+ Hash[*reflection.options[:from].map do |plural|
537
+ [plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
538
+ end.flatten]
539
+ end
540
+
541
+ def demodulate(s)
542
+ s.to_s.gsub('/', '_').to_sym
543
+ end
544
+
545
+ def build_join_table_symbol(association_id, name)
546
+ [name.to_s, association_id.to_s].sort.join("_").to_sym
547
+ end
548
+
549
+ def all_classes_for(association_id, reflection)
550
+ klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
551
+ klasses += klasses.map(&:base_class)
552
+ klasses.uniq
553
+ end
554
+
555
+ def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false)
556
+ # XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
557
+ return unless string
558
+ string = string.dup
559
+ # _logger_debug "devolving #{string} for #{klass}"
560
+ inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
561
+ [klass, klass.base_class, reflection.klass, reflection.klass.base_class])
562
+ inappropriate_classes.map do |klass|
563
+ klass.columns.map do |column|
564
+ [klass.table_name, column.name]
565
+ end.map do |table, column|
566
+ ["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
567
+ end
568
+ end.flatten.sort_by(&:size).reverse.each do |quoted_reference|
569
+ # _logger_debug "devolved #{quoted_reference} to NULL"
570
+ # XXX clause removal would go here
571
+ string.gsub!(quoted_reference, "NULL")
572
+ end
573
+ # _logger_debug "altered to #{string}"
574
+ string
575
+ end
576
+
577
+ def verify_pluralization_of(sym)
578
+ sym = sym.to_s
579
+ singular = sym.singularize
580
+ plural = singular.pluralize
581
+ 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
582
+ end
583
+
584
+ def spiked_create_extension_module(association_id, extensions, identifier = nil)
585
+ module_extensions = extensions.select{|e| e.is_a? Module}
586
+ proc_extensions = extensions.select{|e| e.is_a? Proc }
587
+
588
+ # support namespaced anonymous blocks as well as multiple procs
589
+ proc_extensions.each_with_index do |proc_extension, index|
590
+ module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
591
+ the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
592
+ the_module.class_eval &proc_extension
593
+ module_extensions << the_module
594
+ end
595
+ module_extensions
596
+ end
597
+
598
+ end
599
+ end
600
+ end
@@ -0,0 +1,19 @@
1
+
2
+ =begin rdoc
3
+ 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.
4
+ =end
5
+
6
+ module Rails #:nodoc:
7
+ class Configuration
8
+
9
+ def has_many_polymorphs_options
10
+ ::HasManyPolymorphs.options
11
+ end
12
+
13
+ def has_many_polymorphs_options=(hash)
14
+ ::HasManyPolymorphs.options = HashWithIndifferentAccess.new(hash)
15
+ end
16
+
17
+ end
18
+ end
19
+