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