grouped_scope 0.6.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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