grouped_scope 0.6.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.travis.yml +5 -0
  2. data/CHANGELOG +13 -0
  3. data/Gemfile +4 -6
  4. data/README.md +173 -0
  5. data/lib/grouped_scope.rb +7 -7
  6. data/lib/grouped_scope/arish/associations/association_scope.rb +90 -0
  7. data/lib/grouped_scope/arish/associations/builder/grouped_association.rb +50 -0
  8. data/lib/grouped_scope/arish/associations/builder/grouped_collection_association.rb +32 -0
  9. data/lib/grouped_scope/arish/associations/collection_association.rb +25 -0
  10. data/lib/grouped_scope/arish/base.rb +24 -0
  11. data/lib/grouped_scope/arish/reflection.rb +18 -0
  12. data/lib/grouped_scope/arish/relation/predicate_builer.rb +27 -0
  13. data/lib/grouped_scope/errors.rb +2 -3
  14. data/lib/grouped_scope/self_grouping.rb +59 -23
  15. data/lib/grouped_scope/version.rb +2 -4
  16. data/test/cases/has_many_test.rb +155 -0
  17. data/test/cases/has_many_through_test.rb +51 -0
  18. data/test/cases/reflection_test.rb +62 -0
  19. data/test/cases/self_grouping_test.rb +201 -0
  20. data/test/helper.rb +48 -35
  21. metadata +27 -30
  22. data/README.rdoc +0 -98
  23. data/lib/grouped_scope/association_reflection.rb +0 -54
  24. data/lib/grouped_scope/class_methods.rb +0 -32
  25. data/lib/grouped_scope/core_ext.rb +0 -29
  26. data/lib/grouped_scope/grouping.rb +0 -9
  27. data/lib/grouped_scope/has_many_association.rb +0 -28
  28. data/lib/grouped_scope/has_many_through_association.rb +0 -28
  29. data/lib/grouped_scope/instance_methods.rb +0 -10
  30. data/test/grouped_scope/association_reflection_test.rb +0 -73
  31. data/test/grouped_scope/class_methods_test.rb +0 -51
  32. data/test/grouped_scope/has_many_association_test.rb +0 -156
  33. data/test/grouped_scope/has_many_through_association_test.rb +0 -51
  34. data/test/grouped_scope/self_grouping_test.rb +0 -146
@@ -1,98 +0,0 @@
1
-
2
- == GroupedScope: Has Many Associations IN (GROUPS)
3
-
4
- GroupedScope aims to make two things easier in your ActiveRecord models. First, provide a
5
- easy way to group objects, second, to allow the group to share associated object via existing
6
- has_many relationships. See installation and usage for more details.
7
-
8
- By the way, this plugin has been tested with rails 2.3.2, 2.2.2, and 2.1.1
9
-
10
-
11
- === Installation & Usage
12
-
13
- From your project's RAILS_ROOT, run:
14
-
15
- ./script/plugin install git://github.com/metaskills/grouped_scope.git
16
-
17
- To use GroupedScope on a model it must have a :group_id column.
18
-
19
- class AddGroupId < ActiveRecord::Migration
20
- def self.up
21
- add_column :employees, :group_id, :integer
22
- end
23
- def self.down
24
- remove_column :employees, :group_id
25
- end
26
- end
27
-
28
- Assume the following model.
29
-
30
- class Employee < ActiveRecord::Base
31
- has_many :reports
32
- grouped_scope :reports
33
- end
34
-
35
- By calling grouped_scope on any association you create a new group accessor for each
36
- instance. The object returned will act just like an array and at least include the
37
- current object that called it.
38
-
39
- @employee_one.group # => [#<Employee id: 1, group_id: nil>]
40
-
41
- To group resources, just assign the same :group_id in the schema. Note that in future
42
- versions I will be extending the GroupedScope::Grouping that each object belongs to.
43
- If you do not just want to assign some random integers, then take a look at that model
44
- and the belongs_to :grouping code, schema needed ofcourse
45
-
46
- @employee_one.update_attribute :group_id, 1
47
- @employee_two.update_attribute :group_id, 1
48
- @employee_one.group # => [#<Employee id: 1, group_id: 1>, #<Employee id: 2, group_id: 1>]
49
-
50
- Calling grouped_scope on the :reports association leaves the existing association intact.
51
-
52
- @employee_one.reports # => [#<Report id: 2, employee_id: 1>]
53
- @employee_two.reports # => [#<Report id: 18, employee_id: 2>, #<Report id: 36, employee_id: 2>]
54
-
55
- Now the good part, all associations passed to the grouped_scope method can be called
56
- on the group proxy. The collection will return resources shared by the group.
57
-
58
- @employee_one.group.reports # => [#<Report id: 2, employee_id: 1>,
59
- #<Report id: 18, employee_id: 2>,
60
- #<Report id: 36, employee_id: 2>]
61
-
62
- You can even call named scopes defined on the objects in the collection and association
63
- extensions defined on the original has_many. For instance:
64
-
65
- @employee.group.reports.urgent.assigned_to(user)
66
-
67
-
68
-
69
- === Todo List
70
-
71
- * Go back and start adding some rdocs.
72
- * Add more GroupedScope::Grouping code.
73
- * Add polymorphic support.
74
- * Add/test has_and_belongs_to_many
75
- * Raise errors and/or support :finder_sql/:counter_sql.
76
- * Add a user definable group_id schema.
77
-
78
-
79
- === Helping Our & Running Tests
80
-
81
- Running the test suite is easy to do. Just make sure you have the following gems installed.
82
-
83
- * shoulda
84
- * quitebacktrace
85
- * mocha
86
- * factory_girl
87
-
88
- If you want to run the tests for a specific version of rails in gems (other than the latest),
89
- then you can set the RAILS_VERSION environment variable. For example `env RAILS_VERSION=1.2.6 autotest`.
90
- When doing this you also need to make sure that you download version 4.1 of shoulda (not the gem)
91
- and place its lib contents into `test/lib/shoulda` and `test/lib/shoulda.rb`. The reason is that
92
- the latest should gem require ActiveSupport greater than 2.0.
93
-
94
-
95
-
96
- Copyright (c) 2008 Ken Collins, Decisiv Inc.
97
- Released under the MIT license.
98
-
@@ -1,54 +0,0 @@
1
- module GroupedScope
2
- class AssociationReflection < ActiveRecord::Reflection::AssociationReflection
3
-
4
- ((ActiveRecord::Reflection::AssociationReflection.instance_methods-Class.instance_methods) +
5
- (ActiveRecord::Reflection::AssociationReflection.private_instance_methods-Class.private_instance_methods)).each do |m|
6
- undef_method(m)
7
- end
8
-
9
- attr_accessor :name, :options
10
-
11
- def initialize(active_record,ungrouped_name)
12
- @active_record = active_record
13
- @ungrouped_name = ungrouped_name
14
- @name = :"grouped_scope_#{@ungrouped_name}"
15
- verify_ungrouped_reflection
16
- super(ungrouped_reflection.macro, @name, ungrouped_reflection.options.dup, @active_record)
17
- create_grouped_association
18
- end
19
-
20
- def ungrouped_reflection
21
- @active_record.reflections[@ungrouped_name]
22
- end
23
-
24
- def respond_to?(method, include_private=false)
25
- super || ungrouped_reflection.respond_to?(method,include_private)
26
- end
27
-
28
-
29
- private
30
-
31
- def method_missing(method, *args, &block)
32
- ungrouped_reflection.send(method, *args, &block)
33
- end
34
-
35
- def verify_ungrouped_reflection
36
- if ungrouped_reflection.blank? || ungrouped_reflection.macro.to_s !~ /has_many|has_and_belongs_to_many/
37
- raise ArgumentError, "Cannot create a group scope for :#{@ungrouped_name} because it is not a has_many " +
38
- "or a has_and_belongs_to_many association. Make sure to call grouped_scope after " +
39
- "the has_many associations."
40
- end
41
- end
42
-
43
- def create_grouped_association
44
- active_record.send(macro, name, options)
45
- association_proxy_class = options[:through] ? ActiveRecord::Associations::HasManyThroughAssociation : ActiveRecord::Associations::HasManyAssociation
46
- active_record.send(:collection_reader_method, self, association_proxy_class)
47
-
48
- active_record.reflections[name] = self
49
- active_record.grouped_scopes[@ungrouped_name] = true
50
- options[:grouped_scope] = true
51
- end
52
-
53
- end
54
- end
@@ -1,32 +0,0 @@
1
- module GroupedScope
2
- module ClassMethods
3
-
4
- def grouped_scopes
5
- read_inheritable_attribute(:grouped_scopes) || write_inheritable_attribute(:grouped_scopes, {})
6
- end
7
-
8
- def grouped_scope(*associations)
9
- create_belongs_to_for_grouped_scope
10
- associations.each { |association| AssociationReflection.new(self,association) }
11
- create_grouped_scope_accessor
12
- end
13
-
14
- private
15
-
16
- def create_grouped_scope_accessor
17
- define_method(:group) do
18
- @group ||= SelfGroupping.new(self)
19
- end
20
- end
21
-
22
- def create_belongs_to_for_grouped_scope
23
- grouping_class_name = 'GroupedScope::Grouping'
24
- existing_grouping = reflections[:grouping]
25
- return false if existing_grouping && existing_grouping.macro == :belongs_to && existing_grouping.options[:class_name] == grouping_class_name
26
- belongs_to :grouping, :foreign_key => 'group_id', :class_name => grouping_class_name
27
- end
28
-
29
- end
30
- end
31
-
32
- ActiveRecord::Base.send :extend, GroupedScope::ClassMethods
@@ -1,29 +0,0 @@
1
-
2
- class ActiveRecord::Base
3
-
4
- class << self
5
-
6
- private
7
-
8
- def attribute_condition_with_grouped_scope(*args)
9
- if ActiveRecord::VERSION::STRING >= '2.3.0'
10
- quoted_column_name, argument = args
11
- case argument
12
- when GroupedScope::SelfGroupping then "#{quoted_column_name} IN (?)"
13
- else attribute_condition_without_grouped_scope(quoted_column_name,argument)
14
- end
15
- else
16
- argument = args.first
17
- case argument
18
- when GroupedScope::SelfGroupping then "IN (?)"
19
- else attribute_condition_without_grouped_scope(argument)
20
- end
21
- end
22
- end
23
- alias_method_chain :attribute_condition, :grouped_scope
24
-
25
- end
26
-
27
- end
28
-
29
-
@@ -1,9 +0,0 @@
1
- module GroupedScope
2
- class Grouping < ActiveRecord::Base
3
-
4
-
5
-
6
-
7
- end
8
- end
9
-
@@ -1,28 +0,0 @@
1
- module GroupedScope
2
- module HasManyAssociation
3
-
4
- def self.included(klass)
5
- klass.class_eval do
6
- alias_method_chain :construct_sql, :group_scope
7
- end
8
- end
9
-
10
- def construct_sql_with_group_scope
11
- if @reflection.options[:grouped_scope]
12
- if @reflection.options[:as]
13
- # TODO: Need to add case for polymorphic :as option.
14
- else
15
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} IN (#{@owner.group.quoted_ids})"
16
- @finder_sql << " AND (#{conditions})" if conditions
17
- end
18
- @counter_sql = @finder_sql
19
- else
20
- construct_sql_without_group_scope
21
- end
22
- end
23
-
24
-
25
- end
26
- end
27
-
28
- ActiveRecord::Associations::HasManyAssociation.send :include, GroupedScope::HasManyAssociation
@@ -1,28 +0,0 @@
1
- module GroupedScope
2
- module HasManyThroughAssociation
3
-
4
- def self.included(klass)
5
- klass.class_eval do
6
- alias_method_chain :construct_conditions, :group_scope
7
- end
8
- end
9
-
10
- def construct_conditions_with_group_scope
11
- conditions = construct_conditions_without_group_scope
12
- if @reflection.options[:grouped_scope]
13
- if as = @reflection.options[:as]
14
- # TODO: Need to add case for polymorphic :as option.
15
- else
16
- pattern = "#{@reflection.primary_key_name} = #{@owner.quoted_id}"
17
- replacement = "#{@reflection.primary_key_name} IN (#{@owner.group.quoted_ids})"
18
- conditions.sub!(pattern,replacement)
19
- end
20
- end
21
- conditions
22
- end
23
-
24
-
25
- end
26
- end
27
-
28
- ActiveRecord::Associations::HasManyThroughAssociation.send :include, GroupedScope::HasManyThroughAssociation
@@ -1,10 +0,0 @@
1
- module GroupedScope
2
- module InstanceMethods
3
-
4
- def group
5
- @group ||= SelfGroupping.new(self)
6
- end
7
-
8
-
9
- end
10
- end
@@ -1,73 +0,0 @@
1
- require 'helper'
2
-
3
- class GroupedScope::AssociationReflectionTest < GroupedScope::TestCase
4
-
5
- setup do
6
- setup_environment
7
- end
8
-
9
- context 'For initialization' do
10
-
11
- context 'Raise and exception' do
12
-
13
- setup { @reflection_klass = GroupedScope::AssociationReflection }
14
-
15
- should 'when a association does not exist' do
16
- lambda{ @reflection_klass.new(Employee,:doesnotexist) }.must_raise(ArgumentError)
17
- end
18
-
19
- should 'when the association is not a has_many or a has_and_belongs_to_many' do
20
- Employee.class_eval { belongs_to(:foo) }
21
- lambda{ @reflection_klass.new(Employee,:foo) }.must_raise(ArgumentError)
22
- end
23
-
24
- end
25
-
26
- end
27
-
28
- context 'For #ungrouped_reflection' do
29
-
30
- setup do
31
- @ungrouped_reflection = Employee.reflections[:reports]
32
- @grouped_reflection = Employee.reflections[:grouped_scope_reports]
33
- end
34
-
35
- should 'access ungrouped reflection' do
36
- assert_equal @ungrouped_reflection, @grouped_reflection.ungrouped_reflection
37
- end
38
-
39
- should 'delegate instance methods to #ungrouped_reflection' do
40
- methods = [:class_name,:klass,:table_name,:primary_key_name,:active_record,
41
- :association_foreign_key,:counter_cache_column,:source_reflection]
42
- methods.each do |m|
43
- assert_equal @ungrouped_reflection.send(m), @grouped_reflection.send(m),
44
- "The method #{m.inspect} does not appear to be proxied to the ungrouped reflection."
45
- end
46
- end
47
-
48
- should 'not delegate to #ungrouped_reflection for #options and #name' do
49
- @ungrouped_reflection.name.wont_equal @grouped_reflection.name
50
- @ungrouped_reflection.options.wont_equal @grouped_reflection.options
51
- end
52
-
53
- should 'derive class name to same as ungrouped reflection' do
54
- assert_equal @ungrouped_reflection.send(:derive_class_name), @grouped_reflection.send(:derive_class_name)
55
- end
56
-
57
- should 'derive primary key name to same as ungrouped reflection' do
58
- assert_equal @ungrouped_reflection.send(:derive_primary_key_name), @grouped_reflection.send(:derive_primary_key_name)
59
- end
60
-
61
- should 'honor explicit legacy reports association options like class_name and foreign_key' do
62
- @ungrouped_reflection = LegacyEmployee.reflections[:reports]
63
- @grouped_reflection = LegacyEmployee.reflections[:grouped_scope_reports]
64
- [:class_name,:primary_key_name].each do |m|
65
- assert_equal @ungrouped_reflection.send(m), @grouped_reflection.send(m),
66
- "The method #{m.inspect} does not appear to be proxied to the ungrouped reflection."
67
- end
68
- end
69
-
70
- end
71
-
72
-
73
- end
@@ -1,51 +0,0 @@
1
- require 'helper'
2
-
3
- class GroupedScope::ClassMethodsTest < GroupedScope::TestCase
4
-
5
- setup do
6
- setup_environment
7
- end
8
-
9
- context 'For .grouped_scopes' do
10
-
11
- should 'have a inheritable attribute hash' do
12
- assert_instance_of Hash, Employee.grouped_scopes
13
- end
14
-
15
- should 'add to inheritable attributes with new grouped_scope' do
16
- Employee.grouped_scopes[:foobars] = nil
17
- Employee.class_eval { has_many(:foobars) ; grouped_scope(:foobars) }
18
- assert Employee.grouped_scopes[:foobars]
19
- end
20
-
21
- end
22
-
23
- context 'For .grouped_scope' do
24
-
25
- should 'create a belongs_to :grouping association' do
26
- assert Employee.reflections[:grouping]
27
- end
28
-
29
- should 'not recreate belongs_to :grouping on additional calls' do
30
- Employee.stubs(:belongs_to).never
31
- Employee.class_eval { has_many(:foobars) ; grouped_scope(:foobars) }
32
- end
33
-
34
- should 'create a has_many assoc named :grouped_scope_* using existing association as a suffix' do
35
- grouped_reports_assoc = Employee.reflections[:grouped_scope_reports]
36
- assert_instance_of GroupedScope::AssociationReflection, grouped_reports_assoc
37
- assert FactoryGirl.create(:employee).respond_to?(:grouped_scope_reports)
38
- end
39
-
40
- should 'not add the :grouped_scope option to existing reflection' do
41
- assert_nil Employee.reflections[:reports].options[:grouped_scope]
42
- end
43
-
44
- should 'have added the :grouped_scope option to new grouped reflection' do
45
- assert Employee.reflections[:grouped_scope_reports].options[:grouped_scope]
46
- end
47
-
48
- end
49
-
50
-
51
- end
@@ -1,156 +0,0 @@
1
- require 'helper'
2
-
3
- class GroupedScope::HasManyAssociationTest < GroupedScope::TestCase
4
-
5
- setup do
6
- setup_environment
7
- end
8
-
9
- context 'For an Employee' do
10
-
11
- setup do
12
- @employee = FactoryGirl.create(:employee)
13
- end
14
-
15
- should 'scope existing association to owner' do
16
- assert_sql(/employee_id = #{@employee.id}/) do
17
- @employee.reports(true)
18
- end
19
- end
20
-
21
- should 'scope group association to group' do
22
- assert_sql(/employee_id IN \(#{@employee.id}\)/) do
23
- @employee.group.reports(true)
24
- end
25
- end
26
-
27
- context 'for counting sql' do
28
-
29
- setup do
30
- @e1 = FactoryGirl.create(:employee_with_reports, :group_id => 1)
31
- @e2 = FactoryGirl.create(:employee_with_reports, :group_id => 1)
32
- end
33
-
34
- should 'scope count sql to owner' do
35
- assert_sql(/SELECT count\(\*\)/,/employee_id = #{@e1.id}/) do
36
- @e1.reports(true).count
37
- end
38
- end
39
-
40
- should 'scope count sql to group' do
41
- assert_sql(/SELECT count\(\*\)/,/employee_id IN \(#{@e1.id},#{@e2.id}\)/) do
42
- @e1.group.reports(true).count
43
- end
44
- end
45
-
46
- should 'have a group count equal to sum of seperate owner counts' do
47
- assert_equal @e1.reports(true).count + @e2.reports(true).count, @e2.group.reports(true).count
48
- end
49
-
50
- end
51
-
52
- context 'training association extensions' do
53
-
54
- setup do
55
- @e1 = FactoryGirl.create(:employee_with_urgent_reports, :group_id => 1)
56
- @e2 = FactoryGirl.create(:employee, :group_id => 1)
57
- @urgent_reports = @e1.reports.select(&:urgent_title?)
58
- end
59
-
60
- should 'find urgent report via normal ungrouped association' do
61
- assert_same_elements @urgent_reports, @e1.reports(true).urgent
62
- end
63
-
64
- should 'find urgent report via grouped reflection' do
65
- assert_same_elements @urgent_reports, @e2.group.reports(true).urgent
66
- end
67
-
68
- should 'use assoc extension SQL along with group reflection' do
69
- assert_sql(select_from_reports, where_for_groups, where_for_urgent_title) do
70
- @e2.group.reports.urgent
71
- end
72
- end
73
-
74
- end
75
-
76
- context 'training named scopes' do
77
-
78
- setup do
79
- @e1 = FactoryGirl.create(:employee_with_urgent_reports, :group_id => 1)
80
- @e2 = FactoryGirl.create(:employee, :group_id => 1)
81
- @urgent_titles = @e1.reports.select(&:urgent_title?)
82
- @urgent_bodys = @e1.reports.select(&:urgent_body?)
83
- end
84
-
85
- should 'find urgent reports via normal named scopes by normal owner' do
86
- assert_same_elements @urgent_titles, @e1.reports(true).with_urgent_title
87
- assert_same_elements @urgent_bodys, @e1.reports(true).with_urgent_body
88
- end
89
-
90
- should 'find urgent reports via group reflection by group member' do
91
- assert_same_elements @urgent_titles, @e2.group.reports(true).with_urgent_title
92
- assert_same_elements @urgent_bodys, @e2.group.reports(true).with_urgent_body
93
- end
94
-
95
- should 'use named scope SQL along with group reflection' do
96
- assert_sql(select_from_reports, where_for_groups, where_for_urgent_body, where_for_urgent_title) do
97
- @e2.group.reports.with_urgent_title.with_urgent_body.inspect
98
- end
99
- end
100
-
101
- context 'with will paginate' do
102
-
103
- should 'use group reflection, named scope, and paginate SQL' do
104
- where_ends_with_limits = /WHERE.*LIMIT 2 OFFSET 0/
105
- assert_sql(select_from_reports, where_for_groups, where_for_urgent_body, where_for_urgent_title, where_ends_with_limits) do
106
- @e2.group.reports.with_urgent_title.with_urgent_body.paginate(:page=>1,:per_page=>2)
107
- end
108
- end
109
-
110
- end
111
-
112
- end
113
-
114
- end
115
-
116
- context 'For a LegacyEmployee' do
117
-
118
- setup do
119
- @employee = FactoryGirl.create(:legacy_employee)
120
- end
121
-
122
- should 'scope existing association to owner' do
123
- assert_sql(/"?legacy_reports"?.email = '#{@employee.id}'/) do
124
- @employee.reports(true)
125
- end
126
- end
127
-
128
- should 'scope group association to group' do
129
- assert_sql(/"?legacy_reports"?.email IN \('#{@employee.id}'\)/) do
130
- @employee.group.reports(true)
131
- end
132
- end
133
-
134
- end
135
-
136
-
137
- protected
138
-
139
- def select_from_reports
140
- /SELECT \* FROM "?reports"?/
141
- end
142
-
143
- def where_for_groups
144
- /WHERE.*"?reports"?.employee_id IN \(2,3\)/
145
- end
146
-
147
- def where_for_urgent_body
148
- /WHERE.*body LIKE '%URGENT%'/
149
- end
150
-
151
- def where_for_urgent_title
152
- /WHERE.*"?reports"?."?title"? = 'URGENT'/
153
- end
154
-
155
-
156
- end