required_scopes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,98 @@
1
+ require 'active_record'
2
+ require 'active_record/migration'
3
+
4
+ module RequiredScopes
5
+ module Helpers
6
+ module SystemHelpers
7
+ def migrate(&block)
8
+ migration_class = Class.new(::ActiveRecord::Migration)
9
+ metaclass = migration_class.class_eval { class << self; self; end }
10
+ metaclass.instance_eval { define_method(:up, &block) }
11
+
12
+ ::ActiveRecord::Migration.suppress_messages do
13
+ migration_class.migrate(:up)
14
+ end
15
+ end
16
+
17
+ def define_model_class(name, table_name, &block)
18
+ model_class = Class.new(::ActiveRecord::Base)
19
+ ::Object.send(:remove_const, name) if ::Object.const_defined?(name)
20
+ ::Object.const_set(name, model_class)
21
+ model_class.table_name = table_name
22
+ model_class.class_eval(&block)
23
+ end
24
+
25
+ def create_standard_system_spec_tables!
26
+ migrate do
27
+ drop_table :rec_spec_users rescue nil
28
+ create_table :rec_spec_users do |t|
29
+ t.string :name, :null => false
30
+ t.string :favorite_color
31
+ t.string :favorite_taste
32
+ end
33
+ end
34
+ end
35
+
36
+ def define_color_and_taste_scopes!
37
+ ::User.class_eval do
38
+ must_scope_by :color, :taste
39
+
40
+ scope :red, lambda { where(:favorite_color => 'red') }, :satisfies => :color
41
+ scope :green, lambda { where(:favorite_color => 'green') }, :satisfies => :color
42
+
43
+ scope :salty, lambda { where(:favorite_taste => 'salty') }, :satisfies => :taste
44
+ scope :sweet, lambda { where(:favorite_taste => 'sweet') }, :satisfies => :taste
45
+
46
+ scope :red_and_salty, lambda { where(:favorite_color => 'red', :favorite_taste => 'salty') }, :satisfies => [ :color, :taste ]
47
+ end
48
+ end
49
+
50
+ def should_raise_missing_scopes(triggering_method, required, satisfied, options = { })
51
+ e = result = nil
52
+
53
+ begin
54
+ result = yield
55
+ rescue RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError => rscnse
56
+ e = rscnse
57
+ end
58
+
59
+ raise "Expected a scopes-not-satisfied error, but got none" unless e
60
+
61
+ expected_model_class = options[:model_class] || ::User
62
+
63
+ e.class.should == RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError
64
+ e.model_class.should == expected_model_class
65
+ e.current_relation.should be
66
+ e.current_relation.kind_of?(::ActiveRecord::Relation).should be
67
+ e.triggering_method.should == triggering_method
68
+ e.required_categories.sort_by(&:to_s).should == required.sort_by(&:to_s)
69
+ e.satisfied_categories.sort_by(&:to_s).should == satisfied.sort_by(&:to_s)
70
+
71
+ expected_missing_categories = required - satisfied
72
+ e.missing_categories.sort_by(&:to_s).should == expected_missing_categories.sort_by(&:to_s)
73
+
74
+ e.message.should match(/#{expected_model_class.name}/)
75
+ e.message.should match(/#{expected_missing_categories.sort_by(&:to_s).join(", ")}/)
76
+ end
77
+
78
+ def create_standard_system_spec_models!
79
+ define_model_class(:User, 'rec_spec_users') { }
80
+ end
81
+
82
+ def create_standard_system_spec_instances!
83
+ @red_salty = ::User.create!(:name => 'red-salty', :favorite_color => 'red', :favorite_taste => 'salty')
84
+ @green_salty = ::User.create!(:name => 'green-salty', :favorite_color => 'green', :favorite_taste => 'salty')
85
+ @blue_salty = ::User.create!(:name => 'blue-salty', :favorite_color => 'blue', :favorite_taste => 'salty')
86
+ @red_sweet = ::User.create!(:name => 'red-sweet', :favorite_color => 'red', :favorite_taste => 'sweet')
87
+ @green_sweet = ::User.create!(:name => 'green-sweet', :favorite_color => 'green', :favorite_taste => 'sweet')
88
+ @blue_sweet = ::User.create!(:name => 'blue-sweet', :favorite_color => 'blue', :favorite_taste => 'sweet')
89
+ end
90
+
91
+ def drop_standard_system_spec_tables!
92
+ migrate do
93
+ drop_table :rec_spec_users rescue nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,150 @@
1
+ require 'required_scopes'
2
+ require 'required_scopes/helpers/database_helper'
3
+ require 'required_scopes/helpers/system_helpers'
4
+
5
+ describe "RequiredScopes and associations" do
6
+ include RequiredScopes::Helpers::SystemHelpers
7
+
8
+ before :each do
9
+ @dh = RequiredScopes::Helpers::DatabaseHelper.new
10
+ @dh.setup_activerecord!
11
+
12
+ migrate do
13
+ drop_table :rec_spec_groups rescue nil
14
+ create_table :rec_spec_groups do |t|
15
+ t.string :name, :null => false
16
+ end
17
+
18
+ drop_table :rec_spec_users rescue nil
19
+ create_table :rec_spec_users do |t|
20
+ t.string :name, :null => false
21
+ t.integer :group_id
22
+ t.string :favorite_color
23
+ end
24
+
25
+ drop_table :rec_spec_user_preferences rescue nil
26
+ create_table :rec_spec_user_preferences do |t|
27
+ t.integer :user_id
28
+ t.string :preference
29
+ end
30
+
31
+ drop_table :rec_spec_categories rescue nil
32
+ create_table :rec_spec_categories do |t|
33
+ t.string :name
34
+ end
35
+
36
+ drop_table :rec_spec_categories_users rescue nil
37
+ create_table :rec_spec_categories_users, :id => false do |t|
38
+ t.integer :user_id
39
+ t.integer :category_id
40
+ end
41
+ end
42
+
43
+ define_model_class(:User, :rec_spec_users) do
44
+ must_scope_by :color
45
+ belongs_to :group
46
+ has_one :user_preference
47
+ has_and_belongs_to_many :categories, :join_table => :rec_spec_categories_users
48
+
49
+ scope :red, lambda { where(:favorite_color => :red) }, :satisfies => :color
50
+ end
51
+
52
+ define_model_class(:Group, :rec_spec_groups) do
53
+ has_many :users
54
+ end
55
+
56
+ define_model_class(:UserPreference, :rec_spec_user_preferences) do
57
+ belongs_to :user
58
+
59
+ must_scope_by :prefcolor
60
+ end
61
+
62
+ define_model_class(:Category, :rec_spec_categories) do
63
+ has_and_belongs_to_many :users, :join_table => :rec_spec_categories_users
64
+
65
+ must_scope_by :catcolor
66
+ end
67
+
68
+ @group1 = ::Group.create!(:name => 'Group 1')
69
+ @user1 = ::User.create!(:name => 'g1u1', :favorite_color => 'red', :group => @group1)
70
+ @user2 = ::User.create!(:name => 'g1u2', :favorite_color => 'green', :group => @group1)
71
+
72
+ @user1pref = ::UserPreference.create!(:user => @user1, :preference => 'u1p')
73
+ @user2pref = ::UserPreference.create!(:user => @user2, :preference => 'u2p')
74
+
75
+ @cat1 = ::Category.create!(:name => 'c1')
76
+ @cat2 = ::Category.create!(:name => 'c2')
77
+
78
+ @cat1.users << @user1
79
+ @cat1.users << @user2
80
+ @cat1.save!
81
+
82
+ @cat2.users << @user1
83
+ @cat2.users << @user2
84
+ @cat2.save!
85
+ end
86
+
87
+ after :each do
88
+ migrate do
89
+ drop_table :rec_spec_groups rescue nil
90
+ drop_table :rec_spec_users rescue nil
91
+ drop_table :rec_spec_user_preferences rescue nil
92
+ drop_table :rec_spec_categories rescue nil
93
+ drop_table :rec_spec_categories_users rescue nil
94
+ end
95
+ end
96
+
97
+ it "should require a scope when accessed directly" do
98
+ lambda { ::User.where(:group_id => @group1.id).to_a }.should raise_error(RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError)
99
+ end
100
+
101
+ it "should not require a scope when accessed via an association" do
102
+ @group1.users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
103
+ end
104
+
105
+ it "should not require a scope when accessed via an association, with eager loading" do
106
+ Group.includes(:users).find(@group1.id).users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
107
+ end
108
+
109
+ it "should not require a scope when accessed via #joins" do
110
+ Group.joins(:users).find(@group1.id).users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
111
+ end
112
+
113
+ it "should not require a scope when accessed via #eager_load" do
114
+ Group.eager_load(:users).find(@group1.id).users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
115
+ end
116
+
117
+ it "should not require a scope when accessed via #preload" do
118
+ Group.preload(:users).find(@group1.id).users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
119
+ end
120
+
121
+ it "should not require a scope when accessed via #references" do
122
+ if ::RequiredScopes::ActiveRecord::VersionCompatibility.supports_references_method?
123
+ Group.references(:users).find(@group1.id).users.map(&:id).sort.should == [ @user1, @user2 ].map(&:id).sort
124
+ end
125
+ end
126
+
127
+ it "should not require a scope when accessed via has_one" do
128
+ @user1.user_preference.should == @user1pref
129
+ end
130
+
131
+ it "should not require a scope when accessed via has_one, with eager loading" do
132
+ User.includes(:user_preference).red.find(@user1.id).id.should == @user1pref.id
133
+ end
134
+
135
+ it "should not require a scope when accessed via belongs_to" do
136
+ @user1pref.user.should == @user1
137
+ end
138
+
139
+ it "should not require a scope when accessed via has_one, with eager loading" do
140
+ UserPreference.includes(:user).all_scope_categories_satisfied.find(@user1pref.id).id.should == @user1.id
141
+ end
142
+
143
+ it "should not require a scope when accessed via has_and_belongs_to_many" do
144
+ @user1.categories.map(&:id).sort.should == [ @cat1, @cat2 ].map(&:id).sort
145
+ end
146
+
147
+ it "should not require a scope when accessed via has_and_belongs_to_many, with eager loading" do
148
+ ::User.includes(:categories).all_scope_categories_satisfied.find(@user1).categories.map(&:id).should == [ @cat1, @cat2 ].map(&:id).sort
149
+ end
150
+ end
@@ -0,0 +1,71 @@
1
+ require 'required_scopes'
2
+ require 'required_scopes/helpers/database_helper'
3
+ require 'required_scopes/helpers/system_helpers'
4
+
5
+ describe "RequiredScopes base scope operations" do
6
+ include RequiredScopes::Helpers::SystemHelpers
7
+
8
+ before :each do
9
+ @dh = RequiredScopes::Helpers::DatabaseHelper.new
10
+ @dh.setup_activerecord!
11
+
12
+ create_standard_system_spec_tables!
13
+ create_standard_system_spec_models!
14
+ create_standard_system_spec_instances!
15
+
16
+ ::User.class_eval do
17
+ base_scope_required!
18
+
19
+ base_scope :red, lambda { where(:favorite_color => 'red') }
20
+ base_scope :green, lambda { where(:favorite_color => 'green') }
21
+
22
+ scope :salty, lambda { where(:favorite_taste => 'salty') }
23
+
24
+ class << self
25
+ def blue
26
+ base_scope_satisfied.where(:favorite_color => 'blue')
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ after :each do
33
+ drop_standard_system_spec_tables!
34
+ end
35
+
36
+ it "should require the base scope" do
37
+ e = nil
38
+
39
+ begin
40
+ ::User.all.to_a
41
+ rescue RequiredScopes::Errors::BaseScopeNotSatisfiedError => bsnse
42
+ e = bsnse
43
+ end
44
+
45
+ e.should be
46
+ e.class.should == RequiredScopes::Errors::BaseScopeNotSatisfiedError
47
+ e.model_class.should == ::User
48
+
49
+ e.current_relation.should be
50
+ e.current_relation.kind_of?(::ActiveRecord::Relation).should be
51
+ e.triggering_method.should == :exec_queries
52
+
53
+ e.required_categories.should == [ :base ]
54
+ e.satisfied_categories.should == [ ]
55
+ e.missing_categories.should == [ :base ]
56
+
57
+ e.message.should match(/base scope/i)
58
+ end
59
+
60
+ it "should work if a base scope is used" do
61
+ ::User.red.to_a
62
+ end
63
+
64
+ it "should work if a class method satisfying the base scope is used" do
65
+ ::User.blue.to_a
66
+ end
67
+
68
+ it "should work if you manually tell it that the base scope is satisfied" do
69
+ ::User.base_scope_satisfied.to_a
70
+ end
71
+ end
@@ -0,0 +1,121 @@
1
+ require 'required_scopes'
2
+ require 'required_scopes/helpers/database_helper'
3
+ require 'required_scopes/helpers/system_helpers'
4
+
5
+ describe "RequiredScopes basic operations" do
6
+ include RequiredScopes::Helpers::SystemHelpers
7
+
8
+ before :each do
9
+ @dh = RequiredScopes::Helpers::DatabaseHelper.new
10
+ @dh.setup_activerecord!
11
+
12
+ create_standard_system_spec_tables!
13
+ create_standard_system_spec_models!
14
+ create_standard_system_spec_instances!
15
+
16
+ define_color_and_taste_scopes!
17
+ end
18
+
19
+ after :each do
20
+ drop_standard_system_spec_tables!
21
+ end
22
+
23
+ describe "queries" do
24
+ it "should raise if no categories are applied" do
25
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ ]) { ::User.all.to_a }
26
+ end
27
+
28
+ it "should raise if only one category is applied" do
29
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ :color ]) { ::User.red.to_a }
30
+ end
31
+
32
+ it "should not raise if both categories are individually applied" do
33
+ ::User.red.salty.to_a.should == [ @red_salty ]
34
+ end
35
+
36
+ it "should not raise if both categories are satisfied by a single scope" do
37
+ ::User.red_and_salty.to_a.should == [ @red_salty ]
38
+ end
39
+
40
+ it "should allow further qualifying the scopes, and in the middle" do
41
+ ::User.where("name LIKE 'red%'").red.where("name LIKE '%salty'").salty.where("name LIKE '%d-s%'").to_a.should == [ @red_salty ]
42
+ ::User.where("name LIKE 'green%'").red.salty.to_a.should == [ ]
43
+ ::User.red.where("name LIKE '%sweet'").salty.to_a.should == [ ]
44
+ ::User.red.salty.where("name LIKE '%sweet'").to_a.should == [ ]
45
+ end
46
+
47
+ it "should automatically have a scope that includes all of each category" do
48
+ ::User.red.ignoring_taste.to_a.sort.should == [ @red_salty, @red_sweet ].sort
49
+ ::User.ignoring_color.sweet.to_a.sort.should == [ @red_sweet, @green_sweet, @blue_sweet ].sort
50
+ end
51
+
52
+ it "should still require scopes even if #unscoped is used" do
53
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ ]) { ::User.unscoped.to_a }
54
+ end
55
+
56
+ it "should still require scopes even if #unscoped is used in a block form" do
57
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ ]) { ::User.unscoped { ::User.all.to_a } }
58
+ end
59
+
60
+ it "should allow manually saying that categories are satisfied" do
61
+ ::User.red.scope_category_satisfied(:taste).to_a.sort.should == [ @red_salty, @red_sweet ].to_a
62
+ ::User.scope_category_satisfied(:color).sweet.to_a.sort.should == [ @red_sweet, @green_sweet, @blue_sweet ].to_a
63
+ ::User.scope_categories_satisfied(:color, :taste).to_a.sort.should ==
64
+ [ @red_sweet, @green_sweet, @blue_sweet, @red_salty, @green_salty, @blue_salty ].to_a.sort
65
+ ::User.scope_categories_satisfied(:color, :taste).sweet.to_a.sort.should ==
66
+ [ @red_sweet, @green_sweet, @blue_sweet ].to_a.sort
67
+ end
68
+
69
+ it "should allow manually saying that categories are satisfied, in a block" do
70
+ ran_block = false
71
+ ::User.scope_category_satisfied(:taste) do
72
+ ::User.red.to_a.sort.should == [ @red_salty, @red_sweet].to_a
73
+ ran_block = true
74
+ end
75
+ ran_block.should be
76
+
77
+ ran_block = false
78
+ ::User.scope_category_satisfied(:taste) do
79
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ :taste ]) { ::User.all.to_a }
80
+ ran_block = true
81
+ end
82
+ ran_block.should be
83
+
84
+ ran_block = false
85
+ ::User.scope_category_satisfied(:taste) do
86
+ ::User.red.to_a.sort.should == [ @red_salty, @red_sweet ].sort
87
+ ran_block = true
88
+ end
89
+ ran_block.should be
90
+
91
+ ran_block = false
92
+ ::User.all_scope_categories_satisfied do
93
+ ::User.all.to_a.sort.should == [ @red_salty, @red_sweet, @green_salty, @green_sweet, @blue_salty, @blue_sweet ].sort
94
+ ran_block = true
95
+ end
96
+ ran_block.should be
97
+ end
98
+
99
+ it "should allow saying that categories are satisfied in a class method, in any position" do
100
+ ::User.class_eval do
101
+ class << self
102
+ def red_and_green
103
+ scope_category_satisfied(:color).where(:favorite_color => %w{red green})
104
+ end
105
+
106
+ def green_and_blue
107
+ where(:favorite_color => %w{green blue}).scope_category_satisfied(:color)
108
+ end
109
+
110
+ def red_and_blue
111
+ where(:favorite_color => %w{red blue}).scope_category_satisfied(:color).where(:favorite_taste => %w{salty sweet})
112
+ end
113
+ end
114
+ end
115
+
116
+ ::User.red_and_green.salty.to_a.sort.should == [ @red_salty, @green_salty ].sort
117
+ ::User.green_and_blue.salty.to_a.sort.should == [ @green_salty, @blue_salty ].sort
118
+ ::User.red_and_blue.salty.to_a.sort.should == [ @red_salty, @blue_salty ].sort
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,67 @@
1
+ require 'required_scopes'
2
+ require 'required_scopes/helpers/database_helper'
3
+ require 'required_scopes/helpers/system_helpers'
4
+
5
+ describe "RequiredScopes and inheritance" do
6
+ include RequiredScopes::Helpers::SystemHelpers
7
+
8
+ before :each do
9
+ @dh = RequiredScopes::Helpers::DatabaseHelper.new
10
+ @dh.setup_activerecord!
11
+
12
+ create_standard_system_spec_tables!
13
+ create_standard_system_spec_models!
14
+ create_standard_system_spec_instances!
15
+ end
16
+
17
+ after :each do
18
+ drop_standard_system_spec_tables!
19
+ end
20
+
21
+ it "should combine multiple #must_scope_by declarations" do
22
+ ::User.class_eval do
23
+ must_scope_by :color
24
+ must_scope_by :taste
25
+
26
+ scope :red, lambda { where(:favorite_color => 'red') }, :satisfies => :color
27
+ scope :salty, lambda { where(:favorite_taste => 'salty') }, :satisfies => :taste
28
+ end
29
+
30
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ :taste ]) { ::User.salty.to_a }
31
+ end
32
+
33
+ it "should inherit #must_scope_by in child classes" do
34
+ ::User.class_eval do
35
+ must_scope_by :color
36
+ scope :red, lambda { where(:favorite_color => 'red') }, :satisfies => :color
37
+ end
38
+
39
+ class ::UserSub1 < ::User
40
+ self.table_name = ::User.table_name
41
+
42
+ must_scope_by :taste
43
+ scope :salty, lambda { where(:favorite_taste => 'salty') }, :satisfies => :taste
44
+ end
45
+
46
+ should_raise_missing_scopes(:exec_queries, [ :color, :taste ], [ :taste ], :model_class => ::UserSub1) { ::UserSub1.salty.to_a }
47
+ end
48
+
49
+ it "should allow ignoring a required scope in a subclass" do
50
+ ::User.class_eval do
51
+ must_scope_by :color
52
+ scope :red, lambda { where(:favorite_color => 'red') }, :satisfies => :color
53
+ end
54
+
55
+ class ::UserSub2 < ::User
56
+ self.table_name = ::User.table_name
57
+
58
+ must_scope_by :taste
59
+ scope :salty, lambda { where(:favorite_taste => 'salty') }, :satisfies => :taste
60
+
61
+ ignore_parent_scope_requirement :color
62
+ end
63
+
64
+ ::UserSub2.salty.to_a.should be
65
+ should_raise_missing_scopes(:exec_queries, [ :taste ], [ ], :model_class => ::UserSub2) { ::UserSub2.all.to_a }
66
+ end
67
+ end