├── .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
--------------------------------------------------------------------------------