Commit ad5360bb3928fcc422c134308d56e85d24f15ce0

Authored by AntonioTerceiro
1 parent b1ac1377

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
vendor/plugins/nested_has_many_through/README 0 → 100644
... ... @@ -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
... ...
vendor/plugins/nested_has_many_through/Rakefile 0 → 100644
... ... @@ -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
... ...
vendor/plugins/nested_has_many_through/init.rb 0 → 100644
... ... @@ -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
... ... @@ -0,0 +1,4 @@
  1 +# desc "Explaining what the task does"
  2 +# task :nested_has_many_through do
  3 +# # Task goes here
  4 +# end
0 5 \ No newline at end of file
... ...
vendor/plugins/nested_has_many_through/test/nested_has_many_through_test.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +require 'test/unit'
  2 +
  3 +class NestedHasManyThroughTest < Test::Unit::TestCase
  4 + # Replace this with your real tests.
  5 + def test_this_plugin
  6 + flunk
  7 + end
  8 +end
... ...