Commit ca3fcbeb14ab838a8f58faab3972d36b87c0637d
1 parent
df397df0
Exists in
master
and in
28 other branches
Revert nested_has_many_through but fix has_one :through
Showing
15 changed files
with
748 additions
and
0 deletions
Show diff stats
test/unit/product_test.rb
| ... | ... | @@ -8,6 +8,14 @@ class ProductTest < ActiveSupport::TestCase |
| 8 | 8 | @profile = fast_create(Enterprise) |
| 9 | 9 | end |
| 10 | 10 | |
| 11 | + should 'return associated enterprise region' do | |
| 12 | + @profile.region = fast_create Region, :name => 'Salvador' | |
| 13 | + @profile.save! | |
| 14 | + p = fast_create(Product, :name => 'test product1', :product_category_id => @product_category.id, :enterprise_id => @profile.id) | |
| 15 | + | |
| 16 | + assert_equal @profile.region, p.region | |
| 17 | + end | |
| 18 | + | |
| 11 | 19 | should 'create product' do |
| 12 | 20 | assert_difference Product, :count do |
| 13 | 21 | p = Product.new(:name => 'test product1', :product_category => @product_category, :enterprise_id => @profile.id) | ... | ... |
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +Copyright (c) 2008 Ian White - ian.w.white@gmail.com | |
| 2 | + | |
| 3 | +Permission is hereby granted, free of charge, to any person obtaining | |
| 4 | +a copy of this software and associated documentation files (the | |
| 5 | +"Software"), to deal in the Software without restriction, including | |
| 6 | +without limitation the rights to use, copy, modify, merge, publish, | |
| 7 | +distribute, sublicense, and/or sell copies of the Software, and to | |
| 8 | +permit persons to whom the Software is furnished to do so, subject to | |
| 9 | +the following conditions: | |
| 10 | + | |
| 11 | +The above copyright notice and this permission notice shall be | |
| 12 | +included in all copies or substantial portions of the Software. | |
| 13 | + | |
| 14 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 15 | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 16 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 17 | +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
| 18 | +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| 19 | +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| 20 | +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 0 | 21 | \ No newline at end of file | ... | ... |
| ... | ... | @@ -0,0 +1,92 @@ |
| 1 | +http://plugins.ardes.com > nested_has_many_through | |
| 2 | + | |
| 3 | += nested_has_many_through | |
| 4 | + | |
| 5 | +A fantastic patch/plugin has been floating around for a while: | |
| 6 | + | |
| 7 | +* http://dev.rubyonrails.org/ticket/6461 | |
| 8 | +* http://code.torchbox.com/svn/rails/plugins/nested_has_many_through | |
| 9 | + | |
| 10 | +obrie made the original ticket and Matt Westcott released the first version of | |
| 11 | +the plugin, under the MIT license. Many others have contributed, see the trac | |
| 12 | +ticket for details. | |
| 13 | + | |
| 14 | +Here is a refactored version (I didn't write the original), suitable for edge/2.0-stable | |
| 15 | +with a bunch of acceptance specs. I'm concentrating on plugin usage, once | |
| 16 | +it becomes stable, and well enough speced/understood, then it's time to pester | |
| 17 | +rails-core. | |
| 18 | + | |
| 19 | +== Why republish this on github? | |
| 20 | + | |
| 21 | +* The previous implementations are very poorly speced/tested, so it's pretty | |
| 22 | + hard to refactor and understand this complicated bit of sql-fu, especially | |
| 23 | + when you're aiming at a moving target (edge) | |
| 24 | +* the lastest patches don't apply on edge | |
| 25 | +* github - let's collab to make this better and get a patch accepted, fork away! | |
| 26 | + | |
| 27 | +== Help out | |
| 28 | + | |
| 29 | +I'm releasing 'early and often' in the hope that people will use it and find bugs/problems. | |
| 30 | +Report them at http://ianwhite.lighthouseapp.com, or fork and pull request, yada yada. | |
| 31 | + | |
| 32 | +== History | |
| 33 | + | |
| 34 | +Here's the original description: | |
| 35 | + | |
| 36 | + This plugin makes it possible to define has_many :through relationships that | |
| 37 | + go through other has_many :through relationships, possibly through an | |
| 38 | + arbitrarily deep hierarchy. This allows associations across any number of | |
| 39 | + tables to be constructed, without having to resort to find_by_sql (which isn't | |
| 40 | + a suitable solution if you need to do eager loading through :include as well). | |
| 41 | + | |
| 42 | +== Contributors | |
| 43 | + | |
| 44 | +* Matt Westcott | |
| 45 | +* terceiro | |
| 46 | +* shoe | |
| 47 | +* mhoroschun | |
| 48 | +* Ian White (http://github.com/ianwhite) | |
| 49 | +* Claudio (http://github.com/masterkain) | |
| 50 | + | |
| 51 | +Get in touch if you should be on this list | |
| 52 | + | |
| 53 | +== Show me the money! | |
| 54 | + | |
| 55 | +Here's some models from the specs: | |
| 56 | + | |
| 57 | + class Author < User | |
| 58 | + has_many :posts | |
| 59 | + has_many :categories, :through => :posts, :uniq => true | |
| 60 | + has_many :similar_posts, :through => :categories, :source => :posts | |
| 61 | + has_many :similar_authors, :through => :similar_posts, :source => :author, :uniq => true | |
| 62 | + has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts, :uniq => true | |
| 63 | + has_many :commenters, :through => :posts, :uniq => true | |
| 64 | + end | |
| 65 | + | |
| 66 | + class Post < ActiveRecord::Base | |
| 67 | + belongs_to :author | |
| 68 | + belongs_to :category | |
| 69 | + has_many :comments | |
| 70 | + has_many :commenters, :through => :comments, :source => :user, :uniq => true | |
| 71 | + end | |
| 72 | + | |
| 73 | +The first two has_manys of Author are plain vanilla, the last four are what this plugin enables | |
| 74 | + | |
| 75 | + # has_many through a has_many :through | |
| 76 | + has_many :similar_posts, :through => :categories, :source => :posts | |
| 77 | + | |
| 78 | + # doubly nested has_many :through | |
| 79 | + has_many :similar_authors, :through => :similar_posts, :source => :author, :uniq => true | |
| 80 | + | |
| 81 | + # whoah! | |
| 82 | + has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts, :uniq => true | |
| 83 | + | |
| 84 | + # has_many through a has_many :through in another model | |
| 85 | + has_many :commenters, :through => :posts, :uniq => true | |
| 86 | + | |
| 87 | +== What does it run on? | |
| 88 | + | |
| 89 | +Currently it's running on 2.0, 2.1, and 2.2 stable branches | |
| 90 | + | |
| 91 | +If you want to run the CI suite, then check out garlic_example.rb (The CI suite | |
| 92 | +is being cooked with garlic - git://github.com/ianwhite/garlic) | ... | ... |
| ... | ... | @@ -0,0 +1,77 @@ |
| 1 | +# use pluginized rpsec if it exists | |
| 2 | +rspec_base = File.expand_path(File.dirname(__FILE__) + '/../rspec/lib') | |
| 3 | +$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base) and !$LOAD_PATH.include?(rspec_base) | |
| 4 | + | |
| 5 | +require 'spec/rake/spectask' | |
| 6 | +require 'spec/rake/verify_rcov' | |
| 7 | +require 'rake/rdoctask' | |
| 8 | + | |
| 9 | +plugin_name = 'nested_has_many_through' | |
| 10 | + | |
| 11 | +task :default => :spec | |
| 12 | + | |
| 13 | +desc "Run the specs for #{plugin_name}" | |
| 14 | +Spec::Rake::SpecTask.new(:spec) do |t| | |
| 15 | + t.spec_files = FileList['spec/**/*_spec.rb'] | |
| 16 | + t.spec_opts = ["--colour"] | |
| 17 | +end | |
| 18 | + | |
| 19 | +namespace :spec do | |
| 20 | + desc "Generate RCov report for #{plugin_name}" | |
| 21 | + Spec::Rake::SpecTask.new(:rcov) do |t| | |
| 22 | + t.spec_files = FileList['spec/**/*_spec.rb'] | |
| 23 | + t.rcov = true | |
| 24 | + t.rcov_dir = 'doc/coverage' | |
| 25 | + t.rcov_opts = ['--text-report', '--exclude', "spec/,rcov.rb,#{File.expand_path(File.join(File.dirname(__FILE__),'../../..'))}"] | |
| 26 | + end | |
| 27 | + | |
| 28 | + namespace :rcov do | |
| 29 | + desc "Verify RCov threshold for #{plugin_name}" | |
| 30 | + RCov::VerifyTask.new(:verify => "spec:rcov") do |t| | |
| 31 | + t.threshold = 97.1 | |
| 32 | + t.index_html = File.join(File.dirname(__FILE__), 'doc/coverage/index.html') | |
| 33 | + end | |
| 34 | + end | |
| 35 | + | |
| 36 | + desc "Generate specdoc for #{plugin_name}" | |
| 37 | + Spec::Rake::SpecTask.new(:doc) do |t| | |
| 38 | + t.spec_files = FileList['spec/**/*_spec.rb'] | |
| 39 | + t.spec_opts = ["--format", "specdoc:SPECDOC"] | |
| 40 | + end | |
| 41 | + | |
| 42 | + namespace :doc do | |
| 43 | + desc "Generate html specdoc for #{plugin_name}" | |
| 44 | + Spec::Rake::SpecTask.new(:html => :rdoc) do |t| | |
| 45 | + t.spec_files = FileList['spec/**/*_spec.rb'] | |
| 46 | + t.spec_opts = ["--format", "html:doc/rspec_report.html", "--diff"] | |
| 47 | + end | |
| 48 | + end | |
| 49 | +end | |
| 50 | + | |
| 51 | +task :rdoc => :doc | |
| 52 | +task "SPECDOC" => "spec:doc" | |
| 53 | + | |
| 54 | +desc "Generate rdoc for #{plugin_name}" | |
| 55 | +Rake::RDocTask.new(:doc) do |t| | |
| 56 | + t.rdoc_dir = 'doc' | |
| 57 | + t.main = 'README.rdoc' | |
| 58 | + t.title = "#{plugin_name}" | |
| 59 | + t.template = ENV['RDOC_TEMPLATE'] | |
| 60 | + t.options = ['--line-numbers', '--inline-source'] | |
| 61 | + t.rdoc_files.include('README.rdoc', 'SPECDOC', 'MIT-LICENSE') | |
| 62 | + t.rdoc_files.include('lib/**/*.rb') | |
| 63 | +end | |
| 64 | + | |
| 65 | +namespace :doc do | |
| 66 | + desc "Generate all documentation (rdoc, specdoc, specdoc html and rcov) for #{plugin_name}" | |
| 67 | + task :all => ["spec:doc:html", "spec:doc", "spec:rcov", "doc"] | |
| 68 | +end | |
| 69 | + | |
| 70 | +task :cruise do | |
| 71 | + # run the garlic task, capture the output, if succesful make the docs and copy them to ardes | |
| 72 | + sh "garlic all" | |
| 73 | + `garlic run > .garlic/report.txt` | |
| 74 | + `scp -i ~/.ssh/ardes .garlic/report.txt ardes@ardes.com:~/subdomains/plugins/httpdocs/doc/#{plugin_name}_garlic_report.txt` | |
| 75 | + `cd .garlic/*/vendor/plugins/#{plugin_name}; rake doc:all; scp -i ~/.ssh/ardes -r doc ardes@ardes.com:~/subdomains/plugins/httpdocs/doc/#{plugin_name}` | |
| 76 | + puts "The build is GOOD" | |
| 77 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,49 @@ |
| 1 | + | |
| 2 | +Author (newly created) | |
| 3 | +- #posts should == [] | |
| 4 | +- #categories should == [] | |
| 5 | +- #similar_posts should == [] | |
| 6 | +- #similar_authors should == [] | |
| 7 | +- #commenters should == [] | |
| 8 | + | |
| 9 | +Author (newly created) who creates post with category | |
| 10 | +- #posts should == [post] | |
| 11 | +- #categories should == [category] | |
| 12 | + | |
| 13 | +Author (newly created) who creates post with category and @other_author creates post2 in category | |
| 14 | +- #posts should == [post2] | |
| 15 | +- #categories should == [category] | |
| 16 | +- #similar_posts.should == [post, post2] | |
| 17 | +- #similar_authors.should == [@author, @other_author] | |
| 18 | + | |
| 19 | +Author (newly created) who creates post with category and @other_author creates post2 in category and creates @other_post in @other_category | |
| 20 | +- #similar_posts.should == [@post, @post2] | |
| 21 | +- #posts_by_similar_authors.should == [@post, @post2, @other_post] | |
| 22 | + | |
| 23 | +Commenter use case (a1: p1>c1, a2: p2>c1, p3>c2, a3: p4>c3) | |
| 24 | +- a1.posts should == [p1] | |
| 25 | +- a1.categories should == [c1] | |
| 26 | +- a2.posts should == [p2, p3] | |
| 27 | +- a2.categories should == [c1, c2] | |
| 28 | + | |
| 29 | +Commenter use case (a1: p1>c1, a2: p2>c1, p3>c2, a3: p4>c3) u1 comments on p2 | |
| 30 | +- u1.comments should == [comment] | |
| 31 | +- a1.commenters should be empty | |
| 32 | +- a2.commenters should == [u1] | |
| 33 | +- u1.commented_posts should == [p2] | |
| 34 | +- u1.commented_posts.find_inflamatory(:all) should be empty | |
| 35 | +- u1.commented_posts.inflamatory should be empty | |
| 36 | +- u1.commented_authors should == [a2] | |
| 37 | +- u1.posts_of_interest should == [p1, p2, p3] | |
| 38 | +- u1.categories_of_interest should == [c1, c2] | |
| 39 | + | |
| 40 | +Commenter use case (a1: p1>c1, a2: p2>c1, p3>c2, a3: p4>c3) u1 comments on p2 when p2 is inflamatory | |
| 41 | +- p2 should be inflamatory | |
| 42 | +- u1.commented_posts.find_inflamatory(:all) should == [p2] | |
| 43 | +- u1.posts_of_interest.find_inflamatory(:all) should == [p2] | |
| 44 | +- u1.commented_posts.inflamatory should == [p2] | |
| 45 | +- u1.posts_of_interest.inflamatory should == [p2] | |
| 46 | + | |
| 47 | +Finished in 0.538693 seconds | |
| 48 | + | |
| 49 | +31 examples, 0 failures | ... | ... |
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +* get C2 up to 100% | |
| 2 | + - spec a polymorphic relationship | |
| 3 | + | |
| 4 | +* quote table names | |
| 5 | + | |
| 6 | +* make more use of rails in construct_has_many_or_belongs_to_attributes to reduce brittleness | |
| 7 | + | |
| 8 | +* Add more coverage | |
| 9 | + - scopes | |
| 10 | + - raise an error when nhmt is being used in a perverse way | |
| 0 | 11 | \ No newline at end of file | ... | ... |
| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | +garlic do | |
| 2 | + repo 'nested_has_many_through', :path => '.' | |
| 3 | + | |
| 4 | + repo 'rails', :url => 'git://github.com/rails/rails' | |
| 5 | + repo 'rspec', :url => 'git://github.com/dchelimsky/rspec' | |
| 6 | + repo 'rspec-rails', :url => 'git://github.com/dchelimsky/rspec-rails' | |
| 7 | + | |
| 8 | + # target rails versions | |
| 9 | + ['origin/2-2-stable', 'origin/2-1-stable', 'origin/2-0-stable'].each do |rails| | |
| 10 | + # specify how to prepare app and run CI task | |
| 11 | + target "Rails: #{rails}", :tree_ish => rails do | |
| 12 | + prepare do | |
| 13 | + plugin 'rspec' | |
| 14 | + plugin 'rspec-rails' do | |
| 15 | + `script/generate rspec -f` | |
| 16 | + end | |
| 17 | + plugin 'nested_has_many_through', :clone => true | |
| 18 | + end | |
| 19 | + | |
| 20 | + run do | |
| 21 | + cd "vendor/plugins/nested_has_many_through" do | |
| 22 | + sh "rake spec:rcov:verify" | |
| 23 | + end | |
| 24 | + end | |
| 25 | + end | |
| 26 | + end | |
| 27 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +require 'nested_has_many_through' | |
| 2 | + | |
| 3 | +ActiveRecord::Associations::HasManyThroughAssociation.send :include, NestedHasManyThrough::Association | |
| 4 | + | |
| 5 | +# BC | |
| 6 | +if defined?(ActiveRecord::Reflection::ThroughReflection) | |
| 7 | + ActiveRecord::Reflection::ThroughReflection.send :include, NestedHasManyThrough::Reflection | |
| 8 | +else | |
| 9 | + ActiveRecord::Reflection::AssociationReflection.send :include, NestedHasManyThrough::Reflection | |
| 10 | +end | |
| 0 | 11 | \ No newline at end of file | ... | ... |
vendor/plugins/nested_has_many_through/lib/nested_has_many_through.rb
0 → 100644
| ... | ... | @@ -0,0 +1,148 @@ |
| 1 | +module NestedHasManyThrough | |
| 2 | + module Reflection # :nodoc: | |
| 3 | + def self.included(base) | |
| 4 | + base.send :alias_method_chain, :check_validity!, :nested_has_many_through | |
| 5 | + end | |
| 6 | + | |
| 7 | + def check_validity_with_nested_has_many_through! | |
| 8 | + check_validity_without_nested_has_many_through! | |
| 9 | + rescue ActiveRecord::HasManyThroughSourceAssociationMacroError => e | |
| 10 | + # now we permit has many through to a :though source | |
| 11 | + raise e unless source_reflection.options[:through] | |
| 12 | + end | |
| 13 | + end | |
| 14 | + | |
| 15 | + module Association | |
| 16 | + def self.included(base) | |
| 17 | + base.class_eval do | |
| 18 | + alias_method :original_construct_conditions, :construct_conditions | |
| 19 | + alias_method :original_construct_joins, :construct_joins | |
| 20 | + | |
| 21 | + def construct_conditions | |
| 22 | + if @reflection.macro == :has_one | |
| 23 | + original_construct_conditions | |
| 24 | + else | |
| 25 | + @nested_join_attributes ||= construct_nested_join_attributes | |
| 26 | + "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}" | |
| 27 | + end | |
| 28 | + end | |
| 29 | + | |
| 30 | + def construct_joins(custom_joins = nil) | |
| 31 | + if @reflection.macro == :has_one | |
| 32 | + original_construct_joins(custom_joins) | |
| 33 | + else | |
| 34 | + @nested_join_attributes ||= construct_nested_join_attributes | |
| 35 | + "#{@nested_join_attributes[:joins]} #{custom_joins}" | |
| 36 | + end | |
| 37 | + end | |
| 38 | + end | |
| 39 | + end | |
| 40 | + | |
| 41 | + protected | |
| 42 | + # Given any belongs_to or has_many (including has_many :through) association, | |
| 43 | + # return the essential components of a join corresponding to that association, namely: | |
| 44 | + # | |
| 45 | + # * <tt>:joins</tt>: any additional joins required to get from the association's table | |
| 46 | + # (reflection.table_name) to the table that's actually joining to the active record's table | |
| 47 | + # * <tt>:remote_key</tt>: the name of the key in the join table (qualified by table name) which will join | |
| 48 | + # to a field of the active record's table | |
| 49 | + # * <tt>:local_key</tt>: the name of the key in the local table (not qualified by table name) which will | |
| 50 | + # take part in the join | |
| 51 | + # * <tt>:conditions</tt>: any additional conditions (e.g. filtering by type for a polymorphic association, | |
| 52 | + # or a :conditions clause explicitly given in the association), including a leading AND | |
| 53 | + def construct_nested_join_attributes( reflection = @reflection, | |
| 54 | + association_class = reflection.klass, | |
| 55 | + table_ids = {association_class.table_name => 1}) | |
| 56 | + if reflection.macro == :has_many && reflection.through_reflection | |
| 57 | + construct_has_many_through_attributes(reflection, table_ids) | |
| 58 | + else | |
| 59 | + construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids) | |
| 60 | + end | |
| 61 | + end | |
| 62 | + | |
| 63 | + def construct_has_many_through_attributes(reflection, table_ids) | |
| 64 | + # Construct the join components of the source association, so that we have a path from | |
| 65 | + # the eventual target table of the association up to the table named in :through, and | |
| 66 | + # all tables involved are allocated table IDs. | |
| 67 | + source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids) | |
| 68 | + | |
| 69 | + # Determine the alias of the :through table; this will be the last table assigned | |
| 70 | + # when constructing the source join components above. | |
| 71 | + through_table_alias = through_table_name = reflection.through_reflection.table_name | |
| 72 | + through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 | |
| 73 | + | |
| 74 | + # Construct the join components of the through association, so that we have a path to | |
| 75 | + # the active record's table. | |
| 76 | + through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids) | |
| 77 | + | |
| 78 | + # Any subsequent joins / filters on owner attributes will act on the through association, | |
| 79 | + # so that's what we return for the conditions/keys of the overall association. | |
| 80 | + conditions = through_attrs[:conditions] | |
| 81 | + conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] | |
| 82 | + | |
| 83 | + { | |
| 84 | + :joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [ | |
| 85 | + source_attrs[:joins], | |
| 86 | + through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}", | |
| 87 | + source_attrs[:remote_key], | |
| 88 | + through_table_alias, source_attrs[:local_key], | |
| 89 | + source_attrs[:conditions], | |
| 90 | + through_attrs[:joins], | |
| 91 | + reflection.options[:joins] | |
| 92 | + ], | |
| 93 | + :remote_key => through_attrs[:remote_key], | |
| 94 | + :local_key => through_attrs[:local_key], | |
| 95 | + :conditions => conditions | |
| 96 | + } | |
| 97 | + end | |
| 98 | + | |
| 99 | + | |
| 100 | + # reflection is not has_many :through; it's a standard has_many / belongs_to instead | |
| 101 | + # TODO: see if we can defer to rails code here a bit more | |
| 102 | + def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids) | |
| 103 | + # Determine the alias used for remote_table_name, if any. In all cases this will already | |
| 104 | + # have been assigned an ID in table_ids (either through being involved in a previous join, | |
| 105 | + # or - if it's the first table in the query - as the default value of table_ids) | |
| 106 | + remote_table_alias = remote_table_name = association_class.table_name | |
| 107 | + remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 | |
| 108 | + | |
| 109 | + # Assign a new alias for the local table. | |
| 110 | + local_table_alias = local_table_name = reflection.active_record.table_name | |
| 111 | + if table_ids[local_table_name] | |
| 112 | + table_id = table_ids[local_table_name] += 1 | |
| 113 | + local_table_alias += "_#{table_id}" | |
| 114 | + else | |
| 115 | + table_ids[local_table_name] = 1 | |
| 116 | + end | |
| 117 | + | |
| 118 | + conditions = '' | |
| 119 | + # Add filter for single-table inheritance, if applicable. | |
| 120 | + conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? | |
| 121 | + # Add custom conditions | |
| 122 | + conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] | |
| 123 | + | |
| 124 | + if reflection.macro == :belongs_to | |
| 125 | + if reflection.options[:polymorphic] | |
| 126 | + conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" | |
| 127 | + end | |
| 128 | + { | |
| 129 | + :joins => reflection.options[:joins], | |
| 130 | + :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", | |
| 131 | + :local_key => reflection.primary_key_name, | |
| 132 | + :conditions => conditions | |
| 133 | + } | |
| 134 | + else | |
| 135 | + # Association is has_many (without :through) | |
| 136 | + if reflection.options[:as] | |
| 137 | + conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" | |
| 138 | + end | |
| 139 | + { | |
| 140 | + :joins => "#{reflection.options[:joins]}", | |
| 141 | + :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", | |
| 142 | + :local_key => reflection.klass.primary_key, | |
| 143 | + :conditions => conditions | |
| 144 | + } | |
| 145 | + end | |
| 146 | + end | |
| 147 | + end | |
| 148 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,84 @@ |
| 1 | +# Testing app setup | |
| 2 | + | |
| 3 | +################## | |
| 4 | +# Database schema | |
| 5 | +################## | |
| 6 | + | |
| 7 | +ActiveRecord::Migration.suppress_messages do | |
| 8 | + ActiveRecord::Schema.define(:version => 0) do | |
| 9 | + create_table :users, :force => true do |t| | |
| 10 | + t.column "type", :string | |
| 11 | + end | |
| 12 | + | |
| 13 | + create_table :posts, :force => true do |t| | |
| 14 | + t.column "author_id", :integer | |
| 15 | + t.column "category_id", :integer | |
| 16 | + t.column "inflamatory", :boolean | |
| 17 | + end | |
| 18 | + | |
| 19 | + create_table :categories, :force => true do |t| | |
| 20 | + end | |
| 21 | + | |
| 22 | + create_table :comments, :force => true do |t| | |
| 23 | + t.column "user_id", :integer | |
| 24 | + t.column "post_id", :integer | |
| 25 | + end | |
| 26 | + end | |
| 27 | +end | |
| 28 | + | |
| 29 | +######### | |
| 30 | +# Models | |
| 31 | +# | |
| 32 | +# Domain model is this: | |
| 33 | +# | |
| 34 | +# - authors (type of user) can create posts in categories | |
| 35 | +# - users can comment on posts | |
| 36 | +# - authors have similar_posts: posts in the same categories as ther posts | |
| 37 | +# - authors have similar_authors: authors of the recommended_posts | |
| 38 | +# - authors have posts_of_similar_authors: all posts by similar authors (not just the similar posts, | |
| 39 | +# similar_posts is be a subset of this collection) | |
| 40 | +# - authors have commenters: users who have commented on their posts | |
| 41 | +# | |
| 42 | +class User < ActiveRecord::Base | |
| 43 | + has_many :comments | |
| 44 | + has_many :commented_posts, :through => :comments, :source => :post, :uniq => true | |
| 45 | + has_many :commented_authors, :through => :commented_posts, :source => :author, :uniq => true | |
| 46 | + has_many :posts_of_interest, :through => :commented_authors, :source => :posts_of_similar_authors, :uniq => true | |
| 47 | + has_many :categories_of_interest, :through => :posts_of_interest, :source => :category, :uniq => true | |
| 48 | +end | |
| 49 | + | |
| 50 | +class Author < User | |
| 51 | + has_many :posts | |
| 52 | + has_many :categories, :through => :posts | |
| 53 | + has_many :similar_posts, :through => :categories, :source => :posts | |
| 54 | + has_many :similar_authors, :through => :similar_posts, :source => :author, :uniq => true | |
| 55 | + has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts, :uniq => true | |
| 56 | + has_many :commenters, :through => :posts, :uniq => true | |
| 57 | +end | |
| 58 | + | |
| 59 | +class Post < ActiveRecord::Base | |
| 60 | + | |
| 61 | + # testing with_scope | |
| 62 | + def self.find_inflamatory(*args) | |
| 63 | + with_scope :find => {:conditions => {:inflamatory => true}} do | |
| 64 | + find(*args) | |
| 65 | + end | |
| 66 | + end | |
| 67 | + | |
| 68 | + # only test named_scope in edge | |
| 69 | + named_scope(:inflamatory, :conditions => {:inflamatory => true}) if respond_to?(:named_scope) | |
| 70 | + | |
| 71 | + belongs_to :author | |
| 72 | + belongs_to :category | |
| 73 | + has_many :comments | |
| 74 | + has_many :commenters, :through => :comments, :source => :user, :uniq => true | |
| 75 | +end | |
| 76 | + | |
| 77 | +class Category < ActiveRecord::Base | |
| 78 | + has_many :posts | |
| 79 | +end | |
| 80 | + | |
| 81 | +class Comment < ActiveRecord::Base | |
| 82 | + belongs_to :user | |
| 83 | + belongs_to :post | |
| 84 | +end | |
| 0 | 85 | \ No newline at end of file | ... | ... |
vendor/plugins/nested_has_many_through/spec/models/author_spec.rb
0 → 100644
| ... | ... | @@ -0,0 +1,85 @@ |
| 1 | +require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper')) | |
| 2 | +require File.expand_path(File.join(File.dirname(__FILE__), '../app')) | |
| 3 | + | |
| 4 | +describe Author do | |
| 5 | + describe "(newly created)" do | |
| 6 | + before do | |
| 7 | + @category = Category.create! | |
| 8 | + @other_category = Category.create! | |
| 9 | + @author = Author.create! | |
| 10 | + end | |
| 11 | + | |
| 12 | + it "#posts should == []" do | |
| 13 | + @author.posts.should == [] | |
| 14 | + end | |
| 15 | + | |
| 16 | + it "#categories should == []" do | |
| 17 | + @author.categories.should == [] | |
| 18 | + end | |
| 19 | + | |
| 20 | + it "#similar_posts should == []" do | |
| 21 | + @author.similar_posts.should == [] | |
| 22 | + end | |
| 23 | + | |
| 24 | + it "#similar_authors should == []" do | |
| 25 | + @author.similar_authors.should == [] | |
| 26 | + end | |
| 27 | + | |
| 28 | + it "#commenters should == []" do | |
| 29 | + @author.commenters.should == [] | |
| 30 | + end | |
| 31 | + | |
| 32 | + describe "who creates post with category" do | |
| 33 | + before do | |
| 34 | + @post = Post.create! :author => @author, :category => @category | |
| 35 | + end | |
| 36 | + | |
| 37 | + it "#posts should == [post]" do | |
| 38 | + @author.posts.should == [@post] | |
| 39 | + end | |
| 40 | + | |
| 41 | + it "#categories should == [category]" do | |
| 42 | + @author.categories.should == [@category] | |
| 43 | + end | |
| 44 | + | |
| 45 | + describe "and @other_author creates post2 in category" do | |
| 46 | + | |
| 47 | + before do | |
| 48 | + @other_author = Author.create! | |
| 49 | + @post2 = Post.create! :author => @other_author, :category => @category | |
| 50 | + end | |
| 51 | + | |
| 52 | + it "#posts should == [post2]" do | |
| 53 | + @author.posts.should == [@post] | |
| 54 | + end | |
| 55 | + | |
| 56 | + it "#categories should == [category]" do | |
| 57 | + @author.categories.should == [@category] | |
| 58 | + end | |
| 59 | + | |
| 60 | + it "#similar_posts.should == [post, post2]" do | |
| 61 | + @author.similar_posts.should == [@post, @post2] | |
| 62 | + end | |
| 63 | + | |
| 64 | + it "#similar_authors.should == [@author, @other_author]" do | |
| 65 | + @author.similar_authors.should == [@author, @other_author] | |
| 66 | + end | |
| 67 | + | |
| 68 | + describe "and creates @other_post in @other_category" do | |
| 69 | + before do | |
| 70 | + @other_category = Category.create! | |
| 71 | + @other_post = Post.create! :author => @other_author, :category => @other_category | |
| 72 | + end | |
| 73 | + | |
| 74 | + it "#similar_posts.should == [@post, @post2]" do | |
| 75 | + @author.similar_posts.should == [@post, @post2] | |
| 76 | + end | |
| 77 | + | |
| 78 | + it "#posts_by_similar_authors.should == [@post, @post2, @other_post]" do | |
| 79 | + @author.posts_of_similar_authors.should == [@post, @post2, @other_post] | |
| 80 | + end | |
| 81 | + end | |
| 82 | + end | |
| 83 | + end | |
| 84 | + end | |
| 85 | +end | |
| 0 | 86 | \ No newline at end of file | ... | ... |
vendor/plugins/nested_has_many_through/spec/models/commenter_spec.rb
0 → 100644
| ... | ... | @@ -0,0 +1,109 @@ |
| 1 | +require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper')) | |
| 2 | +require File.expand_path(File.join(File.dirname(__FILE__), '../app')) | |
| 3 | + | |
| 4 | +describe 'Commenter use case (a1: p1>c1, a2: p2>c1, p3>c2, a3: p4>c3)' do | |
| 5 | + before do | |
| 6 | + @c1 = Category.create! | |
| 7 | + @c2 = Category.create! | |
| 8 | + @c3 = Category.create! | |
| 9 | + @a1 = Author.create! | |
| 10 | + @a2 = Author.create! | |
| 11 | + @a3 = Author.create! | |
| 12 | + @p1 = @a1.posts.create! :category => @c1 | |
| 13 | + @p2 = @a2.posts.create! :category => @c1 | |
| 14 | + @p3 = @a2.posts.create! :category => @c2 | |
| 15 | + @p4 = @a3.posts.create! :category => @c3 | |
| 16 | + @a1.reload | |
| 17 | + @a2.reload | |
| 18 | + end | |
| 19 | + | |
| 20 | + it "a1.posts should == [p1]" do | |
| 21 | + @a1.posts.should == [@p1] | |
| 22 | + end | |
| 23 | + | |
| 24 | + it "a1.categories should == [c1]" do | |
| 25 | + @a1.categories.should == [@c1] | |
| 26 | + end | |
| 27 | + | |
| 28 | + it "a2.posts should == [p2, p3]" do | |
| 29 | + @a2.posts.should == [@p2, @p3] | |
| 30 | + end | |
| 31 | + | |
| 32 | + it "a2.categories should == [c1, c2]" do | |
| 33 | + @a2.categories.should == [@c1, @c2] | |
| 34 | + end | |
| 35 | + | |
| 36 | + describe "u1 comments on p2" do | |
| 37 | + before do | |
| 38 | + @u1 = User.create! | |
| 39 | + @comment = @p2.comments.create! :user => @u1 | |
| 40 | + end | |
| 41 | + | |
| 42 | + it "u1.comments should == [comment]" do | |
| 43 | + @u1.comments.should == [@comment] | |
| 44 | + end | |
| 45 | + | |
| 46 | + it "a1.commenters should be empty" do | |
| 47 | + @a1.commenters.should be_empty | |
| 48 | + end | |
| 49 | + | |
| 50 | + it "a2.commenters should == [u1]" do | |
| 51 | + @a2.commenters.should == [@u1] | |
| 52 | + end | |
| 53 | + | |
| 54 | + it "u1.commented_posts should == [p2]" do | |
| 55 | + @u1.commented_posts.should == [@p2] | |
| 56 | + end | |
| 57 | + | |
| 58 | + it "u1.commented_posts.find_inflamatory(:all) should be empty" do | |
| 59 | + @u1.commented_posts.find_inflamatory(:all).should be_empty | |
| 60 | + end | |
| 61 | + | |
| 62 | + if ActiveRecord::Base.respond_to?(:named_scope) | |
| 63 | + it "u1.commented_posts.inflamatory should be empty" do | |
| 64 | + @u1.commented_posts.inflamatory.should be_empty | |
| 65 | + end | |
| 66 | + end | |
| 67 | + | |
| 68 | + it "u1.commented_authors should == [a2]" do | |
| 69 | + @u1.commented_authors.should == [@a2] | |
| 70 | + end | |
| 71 | + | |
| 72 | + it "u1.posts_of_interest should == [p1, p2, p3]" do | |
| 73 | + @u1.posts_of_interest.should == [@p1, @p2, @p3] | |
| 74 | + end | |
| 75 | + | |
| 76 | + it "u1.categories_of_interest should == [c1, c2]" do | |
| 77 | + @u1.categories_of_interest.should == [@c1, @c2] | |
| 78 | + end | |
| 79 | + | |
| 80 | + describe "when p2 is inflamatory" do | |
| 81 | + before do | |
| 82 | + @p2.toggle!(:inflamatory) | |
| 83 | + end | |
| 84 | + | |
| 85 | + it "p2 should be inflamatory" do | |
| 86 | + @p2.should be_inflamatory | |
| 87 | + end | |
| 88 | + | |
| 89 | + it "u1.commented_posts.find_inflamatory(:all) should == [p2]" do | |
| 90 | + # uniq ids is here (and next spec) because eager loading changed behaviour 2.0.2 => edge | |
| 91 | + @u1.commented_posts.find_inflamatory(:all).collect(&:id).uniq.should == [@p2.id] | |
| 92 | + end | |
| 93 | + | |
| 94 | + it "u1.posts_of_interest.find_inflamatory(:all).uniq should == [p2]" do | |
| 95 | + @u1.posts_of_interest.find_inflamatory(:all).collect(&:id).uniq.should == [@p2.id] | |
| 96 | + end | |
| 97 | + | |
| 98 | + if ActiveRecord::Base.respond_to?(:named_scope) | |
| 99 | + it "u1.commented_posts.inflamatory should == [p2]" do | |
| 100 | + @u1.commented_posts.inflamatory.should == [@p2] | |
| 101 | + end | |
| 102 | + | |
| 103 | + it "u1.posts_of_interest.inflamatory should == [p2]" do | |
| 104 | + @u1.posts_of_interest.inflamatory.should == [@p2] | |
| 105 | + end | |
| 106 | + end | |
| 107 | + end | |
| 108 | + end | |
| 109 | +end | ... | ... |
vendor/plugins/nested_has_many_through/spec/spec_helper.rb
0 → 100644
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +# This file is copied to ~/spec when you run 'ruby script/generate rspec' | |
| 2 | +# from the project root directory. | |
| 3 | +ENV["RAILS_ENV"] ||= "test" | |
| 4 | +require File.expand_path(File.join(File.dirname(__FILE__), "../../../../config/environment")) | |
| 5 | +require 'spec/rails' | |
| 6 | + | |
| 7 | +Spec::Runner.configure do |config| | |
| 8 | + config.use_transactional_fixtures = true | |
| 9 | + config.use_instantiated_fixtures = false | |
| 10 | + config.fixture_path = RAILS_ROOT + '/spec/fixtures' | |
| 11 | + | |
| 12 | + # You can declare fixtures for each behaviour like this: | |
| 13 | + # describe "...." do | |
| 14 | + # fixtures :table_a, :table_b | |
| 15 | + # | |
| 16 | + # Alternatively, if you prefer to declare them only once, you can | |
| 17 | + # do so here, like so ... | |
| 18 | + # | |
| 19 | + # config.global_fixtures = :table_a, :table_b | |
| 20 | + # | |
| 21 | + # If you declare global fixtures, be aware that they will be declared | |
| 22 | + # for all of your examples, even those that don't use them. | |
| 23 | +end | |
| 0 | 24 | \ No newline at end of file | ... | ... |