Commit ad5360bb3928fcc422c134308d56e85d24f15ce0
1 parent
b1ac1377
Exists in
master
and in
29 other branches
ActionItem243: importing nested_has_many_through plugin with piston
git-svn-id: https://svn.colivre.coop.br/svn/noosfero/trunk@1591 3f533792-8f58-4932-b0fe-aaf55b0a4547
Showing
6 changed files
with
279 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,49 @@ |
1 | +NestedHasManyThrough | |
2 | +==================== | |
3 | + | |
4 | +This plugin makes it possible to define has_many :through relationships that | |
5 | +go through other has_many :through relationships, possibly through an | |
6 | +arbitrarily deep hierarchy. This allows associations across any number of | |
7 | +tables to be constructed, without having to resort to find_by_sql (which isn't | |
8 | +a suitable solution if you need to do eager loading through :include as well). | |
9 | + | |
10 | +It is hoped that this feature will in time be applied to the Rails core, after | |
11 | +which this plugin will become unnecessary. | |
12 | +See: http://dev.rubyonrails.org/ticket/6461 | |
13 | + | |
14 | +Example | |
15 | +------- | |
16 | + | |
17 | +class Pub < ActiveRecord::Base | |
18 | + belongs_to :city | |
19 | +end | |
20 | + | |
21 | +class City < ActiveRecord::Base | |
22 | + belongs_to :country | |
23 | + has_many :pubs | |
24 | +end | |
25 | + | |
26 | +class Country < ActiveRecord::Base | |
27 | + belongs_to :planet | |
28 | + has_many :cities | |
29 | + has_many :pubs, :through => cities | |
30 | +end | |
31 | + | |
32 | +class Planet < ActiveRecord::Base | |
33 | + belongs_to :star_system | |
34 | + has_many :countries | |
35 | + has_many :cities, :through => :countries | |
36 | + | |
37 | + # Now we go through a has_many :through association - | |
38 | + # something that wasn't previously possible | |
39 | + has_many :pubs, :through => :cities | |
40 | +end | |
41 | + | |
42 | +class StarSystem < ActiveRecord::Base | |
43 | + has_many :planets | |
44 | + has_many :countries, :through => :planets | |
45 | + | |
46 | + # We can also use a has_many :through association for the source | |
47 | + # association; in this case, Country#pubs | |
48 | + has_many :pubs, :through => countries | |
49 | +end | ... | ... |
... | ... | @@ -0,0 +1,22 @@ |
1 | +require 'rake' | |
2 | +require 'rake/testtask' | |
3 | +require 'rake/rdoctask' | |
4 | + | |
5 | +desc 'Default: run unit tests.' | |
6 | +task :default => :test | |
7 | + | |
8 | +desc 'Test the nested_has_many_through plugin.' | |
9 | +Rake::TestTask.new(:test) do |t| | |
10 | + t.libs << 'lib' | |
11 | + t.pattern = 'test/**/*_test.rb' | |
12 | + t.verbose = true | |
13 | +end | |
14 | + | |
15 | +desc 'Generate documentation for the nested_has_many_through plugin.' | |
16 | +Rake::RDocTask.new(:rdoc) do |rdoc| | |
17 | + rdoc.rdoc_dir = 'rdoc' | |
18 | + rdoc.title = 'NestedHasManyThrough' | |
19 | + rdoc.options << '--line-numbers' << '--inline-source' | |
20 | + rdoc.rdoc_files.include('README') | |
21 | + rdoc.rdoc_files.include('lib/**/*.rb') | |
22 | +end | ... | ... |
... | ... | @@ -0,0 +1 @@ |
1 | +require 'nested_has_many_through' | ... | ... |
vendor/plugins/nested_has_many_through/lib/nested_has_many_through.rb
0 → 100644
... | ... | @@ -0,0 +1,195 @@ |
1 | +# Copyright (c) 2007 Matt Westcott | |
2 | +# | |
3 | +# Permission is hereby granted, free of charge, to any person obtaining a copy | |
4 | +# of this software and associated documentation files (the "Software"), to deal | |
5 | +# in the Software without restriction, including without limitation the rights | |
6 | +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
7 | +# copies of the Software, and to permit persons to whom the Software is | |
8 | +# furnished to do so, subject to the following conditions: | |
9 | +# | |
10 | +# The above copyright notice and this permission notice shall be included in | |
11 | +# all copies or substantial portions of the Software. | |
12 | +# | |
13 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
14 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
15 | +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
16 | +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
17 | +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
18 | +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
19 | +# THE SOFTWARE. | |
20 | + | |
21 | +module ActiveRecord #:nodoc: | |
22 | + | |
23 | + module Reflection # :nodoc: | |
24 | + class AssociationReflection < MacroReflection #:nodoc: | |
25 | + def check_validity! | |
26 | + if options[:through] | |
27 | + if through_reflection.nil? | |
28 | + raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) | |
29 | + end | |
30 | + | |
31 | + if source_reflection.nil? | |
32 | + raise HasManyThroughSourceAssociationNotFoundError.new(self) | |
33 | + end | |
34 | + | |
35 | + if options[:source_type] && source_reflection.options[:polymorphic].nil? | |
36 | + raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) | |
37 | + end | |
38 | + | |
39 | + if source_reflection.options[:polymorphic] && options[:source_type].nil? | |
40 | + raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) | |
41 | + end | |
42 | + | |
43 | + # override check_validity! here to always permit has_many associations | |
44 | + # (including has_many :through) to be used as through/source associations | |
45 | + unless [:belongs_to, :has_many].include?(source_reflection.macro) | |
46 | + raise HasManyThroughSourceAssociationMacroError.new(self) | |
47 | + end | |
48 | + end | |
49 | + end | |
50 | + end | |
51 | + end | |
52 | + | |
53 | + module Associations #:nodoc: | |
54 | + class HasManyThroughAssociation < AssociationProxy #:nodoc: | |
55 | + | |
56 | + def initialize(owner, reflection) | |
57 | + super | |
58 | + reflection.check_validity! | |
59 | + end | |
60 | + | |
61 | + def find(*args) | |
62 | + options = Base.send(:extract_options_from_args!, args) | |
63 | + | |
64 | + conditions = construct_conditions | |
65 | + if sanitized_conditions = sanitize_sql(options[:conditions]) | |
66 | + conditions = conditions.dup << " AND (#{sanitized_conditions})" | |
67 | + end | |
68 | + options[:conditions] = conditions | |
69 | + | |
70 | + if options[:order] && @reflection.options[:order] | |
71 | + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" | |
72 | + elsif @reflection.options[:order] | |
73 | + options[:order] = @reflection.options[:order] | |
74 | + end | |
75 | + | |
76 | + options[:select] = construct_select(options[:select]) | |
77 | + options[:from] ||= construct_from | |
78 | + options[:joins] = construct_joins + " #{options[:joins]}" | |
79 | + options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? | |
80 | + | |
81 | + merge_options_from_reflection!(options) | |
82 | + | |
83 | + # Pass through args exactly as we received them. | |
84 | + args << options | |
85 | + @reflection.klass.find(*args) | |
86 | + end | |
87 | + | |
88 | + protected | |
89 | + | |
90 | + # Build SQL conditions from attributes, qualified by table name. | |
91 | + def construct_conditions | |
92 | + if @constructed_conditions.nil? | |
93 | + @join_components ||= construct_join_components | |
94 | + @constructed_conditions = "#{@join_components[:remote_key]} = #{@owner.quoted_id} #{@join_components[:conditions]}" | |
95 | + end | |
96 | + @constructed_conditions | |
97 | + end | |
98 | + | |
99 | + def construct_joins | |
100 | + @join_components ||= construct_join_components | |
101 | + @join_components[:joins] | |
102 | + end | |
103 | + | |
104 | + # Given any belongs_to or has_many (including has_many :through) association, | |
105 | + # return the essential components of a join corresponding to that association, namely: | |
106 | + # joins: any additional joins required to get from the association's table (reflection.table_name) | |
107 | + # to the table that's actually joining to the active record's table | |
108 | + # remote_key: the name of the key in the join table (qualified by table name) which will join | |
109 | + # to a field of the active record's table | |
110 | + # local_key: the name of the key in the local table (not qualified by table name) which will | |
111 | + # take part in the join | |
112 | + # conditions: any additional conditions (e.g. filtering by type for a polymorphic association, | |
113 | + # or a :conditions clause explicitly given in the association), including a leading AND | |
114 | + def construct_join_components(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1}) | |
115 | + | |
116 | + if reflection.macro == :has_many and reflection.through_reflection | |
117 | + # Construct the join components of the source association, so that we have a path from | |
118 | + # the eventual target table of the association up to the table named in :through, and | |
119 | + # all tables involved are allocated table IDs. | |
120 | + source_join_components = construct_join_components(reflection.source_reflection, reflection.klass, table_ids) | |
121 | + # Determine the alias of the :through table; this will be the last table assigned | |
122 | + # when constructing the source join components above. | |
123 | + through_table_alias = through_table_name = reflection.through_reflection.table_name | |
124 | + through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 | |
125 | + | |
126 | + # Construct the join components of the through association, so that we have a path to | |
127 | + # the active record's table. | |
128 | + through_join_components = construct_join_components(reflection.through_reflection, reflection.through_reflection.klass, table_ids) | |
129 | + | |
130 | + # Any subsequent joins / filters on owner attributes will act on the through association, | |
131 | + # so that's what we return for the conditions/keys of the overall association. | |
132 | + conditions = through_join_components[:conditions] | |
133 | + conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] | |
134 | + { | |
135 | + :joins => "#{source_join_components[:joins]} INNER JOIN #{table_name_with_alias(through_table_name, through_table_alias)} ON (#{source_join_components[:remote_key]} = #{through_table_alias}.#{source_join_components[:local_key]}#{source_join_components[:conditions]}) #{through_join_components[:joins]} #{reflection.options[:joins]}", | |
136 | + :remote_key => through_join_components[:remote_key], | |
137 | + :local_key => through_join_components[:local_key], | |
138 | + :conditions => conditions | |
139 | + } | |
140 | + else | |
141 | + # reflection is not has_many :through; it's a standard has_many / belongs_to instead | |
142 | + | |
143 | + # Determine the alias used for remote_table_name, if any. In all cases this will already | |
144 | + # have been assigned an ID in table_ids (either through being involved in a previous join, | |
145 | + # or - if it's the first table in the query - as the default value of table_ids) | |
146 | + remote_table_alias = remote_table_name = association_class.table_name | |
147 | + remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 | |
148 | + | |
149 | + # Assign a new alias for the local table. | |
150 | + local_table_alias = local_table_name = reflection.active_record.table_name | |
151 | + if table_ids[local_table_name] | |
152 | + table_id = table_ids[local_table_name] += 1 | |
153 | + local_table_alias += "_#{table_id}" | |
154 | + else | |
155 | + table_ids[local_table_name] = 1 | |
156 | + end | |
157 | + | |
158 | + conditions = '' | |
159 | + # Add filter for single-table inheritance, if applicable. | |
160 | + conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? | |
161 | + # Add custom conditions | |
162 | + conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] | |
163 | + | |
164 | + if reflection.macro == :belongs_to | |
165 | + if reflection.options[:polymorphic] | |
166 | + conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" | |
167 | + end | |
168 | + { | |
169 | + :joins => reflection.options[:joins], | |
170 | + :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", | |
171 | + :local_key => reflection.primary_key_name, | |
172 | + :conditions => conditions | |
173 | + } | |
174 | + else | |
175 | + # Association is has_many (without :through) | |
176 | + if reflection.options[:as] | |
177 | + conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" | |
178 | + end | |
179 | + { | |
180 | + :joins => "#{reflection.options[:joins]}", | |
181 | + :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", | |
182 | + :local_key => reflection.klass.primary_key, | |
183 | + :conditions => conditions | |
184 | + } | |
185 | + end | |
186 | + end | |
187 | + end | |
188 | + | |
189 | + def table_name_with_alias(table_name, table_alias) | |
190 | + table_name == table_alias ? table_name : "#{table_name} #{table_alias}" | |
191 | + end | |
192 | + | |
193 | + end | |
194 | + end | |
195 | +end | ... | ... |
vendor/plugins/nested_has_many_through/tasks/nested_has_many_through_tasks.rake
0 → 100644
vendor/plugins/nested_has_many_through/test/nested_has_many_through_test.rb
0 → 100644