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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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