├── .gitignore ├── CHANGELOG ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── SPECDOC ├── TODO ├── garlic.rb ├── init.rb ├── lib └── nested_has_many_through.rb └── spec ├── app.rb ├── models ├── author_spec.rb └── commenter_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .garlic 2 | doc/* 3 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | * spec'd and fixed some problems with using named_scope in edge 2 | 3 | * Initial commit 4 | 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == NOTE: rails 2.3 users might want to try the experimental rails-2.3 branch 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 the master branch is running on 2.1, and 2.2 stable branches 90 | 91 | Recent changes have made master incompatible with rails 2.0, if you want to use this 92 | with rails 2.0, then use the rails-2.0 branch. 93 | 94 | If you want to run the CI suite, then check out garlic_example.rb (The CI suite 95 | is being cooked with garlic - git://github.com/ianwhite/garlic) 96 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 78 | -------------------------------------------------------------------------------- /SPECDOC: -------------------------------------------------------------------------------- 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 50 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * belongs_to rel 2 | 3 | * review forks on github 4 | 5 | * get C2 up to 100% 6 | - spec a polymorphic relationship 7 | 8 | * quote table names 9 | 10 | * make more use of rails in construct_has_many_or_belongs_to_attributes to reduce brittleness 11 | 12 | * Add more coverage 13 | - scopes 14 | - raise an error when nhmt is being used in a perverse way -------------------------------------------------------------------------------- /garlic.rb: -------------------------------------------------------------------------------- 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 | ['2-3-stable', '2-2-stable', '2-1-stable'].each do |rails| 10 | target rails, :branch => "origin/#{rails}" do 11 | prepare do 12 | plugin 'rspec' 13 | plugin 'rspec-rails' do 14 | `script/generate rspec -f` 15 | end 16 | plugin 'nested_has_many_through', :clone => true 17 | end 18 | 19 | run do 20 | cd "vendor/plugins/nested_has_many_through" do 21 | sh "rake spec:rcov:verify" 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nested_has_many_through.rb: -------------------------------------------------------------------------------- 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 | def construct_conditions 19 | @nested_join_attributes ||= construct_nested_join_attributes 20 | "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}" 21 | end 22 | 23 | def construct_joins(custom_joins = nil) 24 | @nested_join_attributes ||= construct_nested_join_attributes 25 | "#{@nested_join_attributes[:joins]} #{custom_joins}" 26 | end 27 | end 28 | end 29 | 30 | protected 31 | # Given any belongs_to or has_many (including has_many :through) association, 32 | # return the essential components of a join corresponding to that association, namely: 33 | # 34 | # * :joins: any additional joins required to get from the association's table 35 | # (reflection.table_name) to the table that's actually joining to the active record's table 36 | # * :remote_key: the name of the key in the join table (qualified by table name) which will join 37 | # to a field of the active record's table 38 | # * :local_key: the name of the key in the local table (not qualified by table name) which will 39 | # take part in the join 40 | # * :conditions: any additional conditions (e.g. filtering by type for a polymorphic association, 41 | # or a :conditions clause explicitly given in the association), including a leading AND 42 | def construct_nested_join_attributes( reflection = @reflection, 43 | association_class = reflection.klass, 44 | table_ids = {association_class.table_name => 1}) 45 | if reflection.macro == :has_many && reflection.through_reflection 46 | construct_has_many_through_attributes(reflection, table_ids) 47 | else 48 | construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids) 49 | end 50 | end 51 | 52 | def construct_has_many_through_attributes(reflection, table_ids) 53 | # Construct the join components of the source association, so that we have a path from 54 | # the eventual target table of the association up to the table named in :through, and 55 | # all tables involved are allocated table IDs. 56 | source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids) 57 | 58 | # Determine the alias of the :through table; this will be the last table assigned 59 | # when constructing the source join components above. 60 | through_table_alias = through_table_name = reflection.through_reflection.table_name 61 | through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 62 | 63 | # Construct the join components of the through association, so that we have a path to 64 | # the active record's table. 65 | through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids) 66 | 67 | # Any subsequent joins / filters on owner attributes will act on the through association, 68 | # so that's what we return for the conditions/keys of the overall association. 69 | conditions = through_attrs[:conditions] 70 | conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 71 | 72 | { 73 | :joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [ 74 | source_attrs[:joins], 75 | through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}", 76 | source_attrs[:remote_key], 77 | through_table_alias, source_attrs[:local_key], 78 | source_attrs[:conditions], 79 | through_attrs[:joins], 80 | reflection.options[:joins] 81 | ], 82 | :remote_key => through_attrs[:remote_key], 83 | :local_key => through_attrs[:local_key], 84 | :conditions => conditions 85 | } 86 | end 87 | 88 | 89 | # reflection is not has_many :through; it's a standard has_many / belongs_to instead 90 | # TODO: see if we can defer to rails code here a bit more 91 | def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids) 92 | # Determine the alias used for remote_table_name, if any. In all cases this will already 93 | # have been assigned an ID in table_ids (either through being involved in a previous join, 94 | # or - if it's the first table in the query - as the default value of table_ids) 95 | remote_table_alias = remote_table_name = association_class.table_name 96 | remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 97 | 98 | # Assign a new alias for the local table. 99 | local_table_alias = local_table_name = reflection.active_record.table_name 100 | if table_ids[local_table_name] 101 | table_id = table_ids[local_table_name] += 1 102 | local_table_alias += "_#{table_id}" 103 | else 104 | table_ids[local_table_name] = 1 105 | end 106 | 107 | conditions = '' 108 | # Add type_condition, if applicable 109 | conditions += " AND #{association_class.send(:type_condition, remote_table_alias)}" unless association_class.descends_from_active_record? 110 | # Add custom conditions 111 | conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] 112 | 113 | if reflection.macro == :belongs_to 114 | if reflection.options[:polymorphic] 115 | conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" 116 | end 117 | { 118 | :joins => reflection.options[:joins], 119 | :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", 120 | :local_key => reflection.primary_key_name, 121 | :conditions => conditions 122 | } 123 | else 124 | # Association is has_many (without :through) 125 | if reflection.options[:as] 126 | conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" 127 | end 128 | { 129 | :joins => "#{reflection.options[:joins]}", 130 | :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", 131 | :local_key => reflection.klass.primary_key, 132 | :conditions => conditions 133 | } 134 | end 135 | end 136 | end 137 | end -------------------------------------------------------------------------------- /spec/app.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/models/author_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/models/commenter_spec.rb: -------------------------------------------------------------------------------- 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 110 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 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 --------------------------------------------------------------------------------