has_many_polymorphs 2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,160 @@
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
+ {@reflection.options[:polymorphic_key] => record.id,
148
+ @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",
149
+ @reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ?
150
+ {@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships
151
+ end
152
+
153
+ def build(attrs = nil) #:nodoc:
154
+ raise PolymorphicMethodNotSupportedError, "You can't associate new records."
155
+ end
156
+
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,69 @@
1
+ require 'initializer' unless defined? ::Rails::Initializer
2
+ require 'action_controller/dispatcher' unless defined? ::ActionController::Dispatcher
3
+
4
+ module HasManyPolymorphs
5
+
6
+ =begin rdoc
7
+ 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.
8
+
9
+ 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:
10
+ Rails::Initializer.run do |config|
11
+ # your other configuration here
12
+
13
+ config.after_initialize do
14
+ config.has_many_polymorphs_options['requirements'] << 'lib/my_extension'
15
+ end
16
+ end
17
+
18
+ =end
19
+
20
+ DEFAULT_OPTIONS = {
21
+ :file_pattern => "#{RAILS_ROOT}/app/models/**/*.rb",
22
+ :file_exclusions => ['svn', 'CVS', 'bzr'],
23
+ :methods => ['has_many_polymorphs', 'acts_as_double_polymorphic_join'],
24
+ :requirements => []}
25
+
26
+ mattr_accessor :options
27
+ @@options = HashWithIndifferentAccess.new(DEFAULT_OPTIONS)
28
+
29
+ # Dispatcher callback to load polymorphic relationships from the top down.
30
+ def self.autoload
31
+
32
+ _logger_debug "autoload hook invoked"
33
+
34
+ options[:requirements].each do |requirement|
35
+ _logger_warn "forcing requirement load of #{requirement}"
36
+ require requirement
37
+ end
38
+
39
+ Dir.glob(options[:file_pattern]).each do |filename|
40
+ next if filename =~ /#{options[:file_exclusions].join("|")}/
41
+ open filename do |file|
42
+ if file.grep(/#{options[:methods].join("|")}/).any?
43
+ begin
44
+ model = File.basename(filename)[0..-4].camelize
45
+ _logger_warn "preloading parent model #{model}"
46
+ model.constantize
47
+ rescue Object => e
48
+ _logger_warn "#{model} could not be preloaded: #{e.inspect}"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ class Rails::Initializer #:nodoc:
58
+ # Make sure it gets loaded in the console, tests, and migrations
59
+ def after_initialize_with_autoload
60
+ after_initialize_without_autoload
61
+ HasManyPolymorphs.autoload
62
+ end
63
+ alias_method_chain :after_initialize, :autoload
64
+ end
65
+
66
+ ActionController::Dispatcher.to_prepare(:has_many_polymorphs_autoload) do
67
+ # Make sure it gets loaded in the app
68
+ HasManyPolymorphs.autoload
69
+ end
@@ -0,0 +1,60 @@
1
+
2
+ module ActiveRecord
3
+ class Base
4
+
5
+ class << self
6
+
7
+ # Interprets a polymorphic row from a unified SELECT, returning the appropriate ActiveRecord instance. Overrides ActiveRecord::Base.instantiate_without_callbacks.
8
+ def instantiate_with_polymorphic_checks(record)
9
+ if record['polymorphic_parent_class']
10
+ reflection = record['polymorphic_parent_class'].constantize.reflect_on_association(record['polymorphic_association_id'].to_sym)
11
+ # _logger_debug "Instantiating a polymorphic row for #{record['polymorphic_parent_class']}.reflect_on_association(:#{record['polymorphic_association_id']})"
12
+
13
+ # rewrite the record with the right column names
14
+ table_aliases = reflection.options[:table_aliases].dup
15
+ record = Hash[*table_aliases.keys.map {|key| [key, record[table_aliases[key]]] }.flatten]
16
+
17
+ # find the real child class
18
+ klass = record["#{self.table_name}.#{reflection.options[:polymorphic_type_key]}"].constantize
19
+ if sti_klass = record["#{klass.table_name}.#{klass.inheritance_column}"]
20
+ klass = klass.class_eval do compute_type(sti_klass) end # in case of namespaced STI models
21
+ end
22
+
23
+ # check that the join actually joined to something
24
+ unless (child_id = record["#{self.table_name}.#{reflection.options[:polymorphic_key]}"]) == record["#{klass.table_name}.#{klass.primary_key}"]
25
+ raise ActiveRecord::Associations::PolymorphicError,
26
+ "Referential integrity violation; child <#{klass.name}:#{child_id}> was not found for #{reflection.name.inspect}"
27
+ end
28
+
29
+ # eject the join keys
30
+ # XXX not very readable
31
+ record = Hash[*record._select do |column, value|
32
+ column[/^#{klass.table_name}/]
33
+ end.map do |column, value|
34
+ [column[/\.(.*)/, 1], value]
35
+ end.flatten]
36
+
37
+ # allocate and assign values
38
+ returning(klass.allocate) do |obj|
39
+ obj.instance_variable_set("@attributes", record)
40
+ obj.instance_variable_set("@attributes_cache", Hash.new)
41
+
42
+ if obj.respond_to_without_attributes?(:after_find)
43
+ obj.send(:callback, :after_find)
44
+ end
45
+
46
+ if obj.respond_to_without_attributes?(:after_initialize)
47
+ obj.send(:callback, :after_initialize)
48
+ end
49
+
50
+ end
51
+ else
52
+ instantiate_without_polymorphic_checks(record)
53
+ end
54
+ end
55
+
56
+ alias_method_chain :instantiate, :polymorphic_checks
57
+ end
58
+
59
+ end
60
+ end