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
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ree
data/CHANGELOG CHANGED
@@ -1,6 +1,19 @@
1
1
 
2
2
  = master
3
3
 
4
+ = 3.1.0 (unreleased)
5
+
6
+ * Works with ActiveRecord 3.1
7
+
8
+ * The group object is now an ActiveRecord::Relation so you can further scope it.
9
+
10
+ * New group scoped #blank? and #present? which simply checks if the proxy owner has a group set.
11
+ This allows us to tune the SQL generated to IN statements only when needed, even if a grouped
12
+ scope is being used.
13
+
14
+ * New group.ids_sql which is an Arel SQL literal. Avoids large groups IDs and better query plans.
15
+
16
+
4
17
  = 0.6.0 (May 06, 2009)
5
18
 
6
19
  * ActiveRecord 2.3.14 compatibility.
data/Gemfile CHANGED
@@ -7,16 +7,14 @@ ar_version = spec.dependencies.detect{ |d|d.name == 'activerecord' }.requirement
7
7
 
8
8
  gem 'sqlite3', '1.3.4'
9
9
  gem 'activerecord', ar_version, :require => 'active_record'
10
- gem 'will_paginate', '2.3.16'
11
10
 
12
11
  group :development do
13
- gem 'rake', '0.8.7'
12
+ gem 'rake', '~> 0.9.2'
14
13
  end
15
14
 
16
15
  group :test do
17
- gem 'minitest', '2.5.1'
18
- gem 'mini_shoulda', '0.4.0'
19
- gem 'factory_girl', '2.1.0'
20
- gem 'mocha', '0.10.0'
16
+ gem 'minitest', '~> 2.8.1'
17
+ gem 'factory_girl', '~> 2.3.2'
18
+ gem 'mocha', '~> 0.10.0'
21
19
  end
22
20
 
@@ -0,0 +1,173 @@
1
+
2
+ # GroupedScope: Has Many Associations IN (GROUPS)
3
+
4
+ <img src="http://metaskills.net/assets/jack.png" alt="Jack Has Many Things" width="320" height="214" style="float:right; margin:0 0 15px 15px; background-color:#fff; padding:13px;">
5
+
6
+ GroupedScope provides an easy way to group objects and to allow those groups to share association collections via existing `has_many` relationships. You may enjoy my original article titled [*Jack has_many :things*](http://metaskills.net/2008/09/28/jack-has_many-things/).
7
+
8
+
9
+ ## Installation
10
+
11
+ Install the gem with bundler. We follow a semantic versioning format that tracks ActiveRecord's minor version. So this means to use the latest 3.1.x version of GroupedScope with any ActiveRecord 3.1 version.
12
+
13
+ ```ruby
14
+ gem 'grouped_scope', '~> 3.1.0'
15
+ ```
16
+
17
+
18
+ ## Setup
19
+
20
+ To use GroupedScope on a model it must have a `:group_id` column.
21
+
22
+ ```ruby
23
+ class AddGroupId < ActiveRecord::Migration
24
+ def up
25
+ add_column :employees, :group_id, :integer
26
+ end
27
+ def down
28
+ remove_column :employees, :group_id
29
+ end
30
+ end
31
+ ```
32
+
33
+
34
+ ## General Usage
35
+
36
+ Assume the following model.
37
+
38
+ ```ruby
39
+ class Employee < ActiveRecord::Base
40
+ has_many :reports
41
+ grouped_scope :reports
42
+ end
43
+ ```
44
+
45
+ By calling grouped_scope on any association you create a new group accessor for each
46
+ instance. The object returned will act just like an array and at least include the
47
+ current object that called it.
48
+
49
+ ```ruby
50
+ @employee_one.group # => [#<Employee id: 1, group_id: nil>]
51
+ ```
52
+
53
+ To group resources, just assign the same `:group_id` to each record in that group.
54
+
55
+ ```ruby
56
+ @employee_one.update_attribute :group_id, 1
57
+ @employee_two.update_attribute :group_id, 1
58
+ @employee_one.group # => [#<Employee id: 1, group_id: 1>, #<Employee id: 2, group_id: 1>]
59
+ ```
60
+
61
+ Calling grouped_scope on the :reports association leaves the existing association intact.
62
+
63
+ ```ruby
64
+ @employee_one.reports # => [#<Report id: 2, employee_id: 1>]
65
+ @employee_two.reports # => [#<Report id: 18, employee_id: 2>, #<Report id: 36, employee_id: 2>]
66
+ ```
67
+
68
+ Now the good part, all associations passed to the grouped_scope method can be called
69
+ on the group proxy. The collection will return resources shared by the group.
70
+
71
+ ```ruby
72
+ @employee_one.group.reports # => [#<Report id: 2, employee_id: 1>,
73
+ #<Report id: 18, employee_id: 2>,
74
+ #<Report id: 36, employee_id: 2>]
75
+ ```
76
+
77
+ You can even call scopes or association extensions defined on the objects in the collection
78
+ defined on the original association. For instance:
79
+
80
+ ```ruby
81
+ @employee.group.reports.urgent.assigned_to(user)
82
+ ```
83
+
84
+
85
+ ## Advanced Usage
86
+
87
+ The group scoped object can respond to either `blank?` or `present?` which checks the group's
88
+ target `group_id` presence or not. We use this internally so that grouped scopes only use grouping
89
+ SQL when absolutely needed.
90
+
91
+ ```ruby
92
+ @employee_one = Employee.create :group_id => nil
93
+ @employee_two = Employee.create :group_id => 38
94
+
95
+ @employee_one.group.blank? # => true
96
+ @employee_two.group.present? # => true
97
+ ```
98
+
99
+ The object returned by the `#group` method is an ActiveRecord relation on the targets class,
100
+ in this case `Employee`. Given this, you can further scope the grouped proxy if needed. Below,
101
+ we use the `:email_present` scope to refine the group down.
102
+
103
+ ```ruby
104
+ class Employee < ActiveRecord::Base
105
+ has_many :reports
106
+ grouped_scope :reports
107
+ scope :email_present, where("email IS NOT NULL")
108
+ end
109
+
110
+ @employee_one = Employee.create :group_id => 5, :name => 'Ken'
111
+ @employee_two = Employee.create :group_id => 5, :name => 'MetaSkills', :email => 'ken@metaskills.net'
112
+
113
+ # Only one employee is returned now.
114
+ @employee_one.group.email_present # => [#<Employee id: 1, group_id: 5, name: 'MetaSkills', email: 'ken@metaskills.net']
115
+ ```
116
+
117
+ We always use raw SQL to get the group ids vs. mapping them to an array and using those in scopes.
118
+ This means that large groups can avoid pushing down hundreds of keys in SQL form. So given an employee
119
+ with a `group_id` of `43` and calling `@employee.group.reports`, you would get something similar to
120
+ the following SQL.
121
+
122
+ ```sql
123
+ SELECT "reports".*
124
+ FROM "reports"
125
+ WHERE "reports"."employee_id" IN (
126
+ SELECT "employees"."id"
127
+ FROM "employees"
128
+ WHERE "employees"."group_id" = 43
129
+ )
130
+ ```
131
+
132
+ You can pass the group scoped object as a predicate to ActiveRecord's relation interface. In past
133
+ versions, this would have treated the group object as an array of IDs. The new behavior is to return
134
+ a SQL literal to be used with IN statements. So note, the following would generate SQL similar to
135
+ the one above.
136
+
137
+ ```ruby
138
+ Employee.where(:group_id => @employee.group).all
139
+ ```
140
+
141
+ If you need more control and you are working with the group at a lower level, you can always
142
+ use the `#ids` or `#ids_sql` methods on the group.
143
+
144
+ ```ruby
145
+ # Returns primary key array.
146
+ @employee.group.ids # => [33, 58, 240]
147
+
148
+ # Returns a Arel::Nodes::SqlLiteral object.
149
+ @employee.group.ids_sql # => 'SELECT "employees"."id" FROM "employees" WHERE "employees"."group_id" = 33'
150
+ ```
151
+
152
+
153
+ ## Todo List
154
+
155
+ * Raise errors for :finder_sql/:counter_sql.
156
+ * Add a user definable group_id schema.
157
+ * Remove SelfGrouping#with_relation, has not yet proved useful.
158
+
159
+
160
+
161
+ ## Testing
162
+
163
+ Simple! Just clone the repo, then run `bundle install` and `bundle exec rake`. The tests will begin to run. We also use Travis CI to run our tests too. Current build status is:
164
+
165
+ [![Build Status](https://secure.travis-ci.org/metaskills/grouped_scope.png)](http://travis-ci.org/metaskills/grouped_scope)
166
+
167
+
168
+
169
+ ## License
170
+
171
+ Released under the MIT license.
172
+ Copyright (c) 2011 Ken Collins
173
+
@@ -1,11 +1,11 @@
1
1
  require 'active_record/version'
2
2
  require 'grouped_scope/errors'
3
- require 'grouped_scope/grouping'
4
3
  require 'grouped_scope/self_grouping'
5
- require 'grouped_scope/association_reflection'
6
- require 'grouped_scope/class_methods'
7
- require 'grouped_scope/has_many_association'
8
- require 'grouped_scope/has_many_through_association'
9
- require 'grouped_scope/core_ext'
10
- require 'grouped_scope/version'
11
4
 
5
+ require 'grouped_scope/arish/reflection'
6
+ require 'grouped_scope/arish/associations/collection_association'
7
+ require 'grouped_scope/arish/associations/builder/grouped_association'
8
+ require 'grouped_scope/arish/associations/builder/grouped_collection_association'
9
+ require 'grouped_scope/arish/associations/association_scope'
10
+ require 'grouped_scope/arish/relation/predicate_builer'
11
+ require 'grouped_scope/arish/base'
@@ -0,0 +1,90 @@
1
+ module GroupedScope
2
+ module Arish
3
+ module Associations
4
+ class AssociationScope < ActiveRecord::Associations::AssociationScope
5
+
6
+
7
+ private
8
+
9
+ # A direct copy of of ActiveRecord's AssociationScope#add_constraints. If this was
10
+ # in chunks, it would be easier to hook into. This more elegant version which supers
11
+ # up will only work for the has_many. https://gist.github.com/1434980
12
+ #
13
+ # We will just have to monitor rails every now and then and update this. Thankfully this
14
+ # copy is only used in a group scope. FYI, our one line change is commented below.
15
+ def add_constraints(scope)
16
+ tables = construct_tables
17
+
18
+ chain.each_with_index do |reflection, i|
19
+ table, foreign_table = tables.shift, tables.first
20
+
21
+ if reflection.source_macro == :has_and_belongs_to_many
22
+ join_table = tables.shift
23
+
24
+ scope = scope.joins(join(
25
+ join_table,
26
+ table[reflection.association_primary_key].
27
+ in(join_table[reflection.association_foreign_key])
28
+ ))
29
+
30
+ table, foreign_table = join_table, tables.first
31
+ end
32
+
33
+ if reflection.source_macro == :belongs_to
34
+ if reflection.options[:polymorphic]
35
+ key = reflection.association_primary_key(klass)
36
+ else
37
+ key = reflection.association_primary_key
38
+ end
39
+
40
+ foreign_key = reflection.foreign_key
41
+ else
42
+ key = reflection.foreign_key
43
+ foreign_key = reflection.active_record_primary_key
44
+ end
45
+
46
+ conditions = self.conditions[i]
47
+
48
+ if reflection == chain.last
49
+ # GroupedScope changed this line.
50
+ # scope = scope.where(table[key].eq(owner[foreign_key]))
51
+ scope = if owner.group.present?
52
+ scope.where(table[key].in(owner.group.ids_sql))
53
+ else
54
+ scope.where(table[key].eq(owner[foreign_key]))
55
+ end
56
+
57
+ if reflection.type
58
+ scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
59
+ end
60
+
61
+ conditions.each do |condition|
62
+ if options[:through] && condition.is_a?(Hash)
63
+ condition = { table.name => condition }
64
+ end
65
+
66
+ scope = scope.where(interpolate(condition))
67
+ end
68
+ else
69
+ constraint = table[key].eq(foreign_table[foreign_key])
70
+
71
+ if reflection.type
72
+ type = chain[i + 1].klass.base_class.name
73
+ constraint = constraint.and(table[reflection.type].eq(type))
74
+ end
75
+
76
+ scope = scope.joins(join(foreign_table, constraint))
77
+
78
+ unless conditions.empty?
79
+ scope = scope.where(sanitize(conditions, table))
80
+ end
81
+ end
82
+ end
83
+
84
+ scope
85
+ end
86
+
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,50 @@
1
+ module GroupedScope
2
+ module Arish
3
+ module Associations
4
+ module Builder
5
+ class GroupedAssociation
6
+
7
+ attr_reader :model, :ungrouped_name, :ungrouped_reflection, :grouped_name, :grouped_options
8
+
9
+ def self.build(model, *association_names)
10
+ association_names.each { |ungrouped_name| new(model, ungrouped_name).build }
11
+ end
12
+
13
+ def initialize(model, ungrouped_name)
14
+ @model = model
15
+ @ungrouped_name = ungrouped_name
16
+ @ungrouped_reflection = find_ungrouped_reflection
17
+ @grouped_name = :"grouped_scope_#{ungrouped_name}"
18
+ @grouped_options = copy_ungrouped_reflection_options
19
+ end
20
+
21
+ def build
22
+ model.send(ungrouped_reflection.macro, grouped_name, grouped_options).tap do |grouped_reflection|
23
+ grouped_reflection.grouped_scope = true
24
+ model.grouped_reflections = model.grouped_reflections.merge(ungrouped_name => grouped_reflection)
25
+ define_grouped_scope_reader(model)
26
+ end
27
+ end
28
+
29
+
30
+ private
31
+
32
+ def define_grouped_scope_reader(model)
33
+ model.send(:define_method, :group) do
34
+ @group ||= GroupedScope::SelfGroupping.new(self)
35
+ end
36
+ end
37
+
38
+ def find_ungrouped_reflection
39
+ model.reflections[ungrouped_name]
40
+ end
41
+
42
+ def copy_ungrouped_reflection_options
43
+ ungrouped_reflection.options.dup
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ module GroupedScope
2
+ module Arish
3
+ module Associations
4
+ module Builder
5
+ class GroupedCollectionAssociation < GroupedAssociation
6
+
7
+ private
8
+
9
+ def find_ungrouped_reflection
10
+ reflection = model.reflections[ungrouped_name]
11
+ if reflection.blank? || [:has_many, :has_and_belongs_to_many].exclude?(reflection.macro)
12
+ msg = "Cannot create a group scope for #{ungrouped_name.inspect}. Either the reflection is blank or not supported. " +
13
+ "Make sure to call grouped_scope after the association you are trying to extend has been defined."
14
+ raise ArgumentError, msg
15
+ end
16
+ reflection
17
+ end
18
+
19
+ def copy_ungrouped_reflection_options
20
+ ungrouped_reflection.options.dup.tap do |options|
21
+ options[:class_name] = ungrouped_reflection.class_name
22
+ if ungrouped_reflection.source_reflection && options[:source].blank?
23
+ options[:source] = ungrouped_reflection.source_reflection.name
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module GroupedScope
2
+ module Arish
3
+ module Associations
4
+ module CollectionAssociation
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ module InstanceMethods
9
+
10
+ def association_scope
11
+ if reflection.grouped_scope?
12
+ @association_scope ||= Associations::AssociationScope.new(self).scope if klass
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ ActiveRecord::Associations::CollectionAssociation.send :include, GroupedScope::Arish::Associations::CollectionAssociation
@@ -0,0 +1,24 @@
1
+ module GroupedScope
2
+ module Arish
3
+ module Base
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :grouped_reflections, :instance_reader => false, :instance_writer => false
9
+ self.grouped_reflections = {}.freeze
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ def grouped_scope(*association_names)
15
+ Associations::Builder::GroupedCollectionAssociation.build(self, *association_names)
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Base.send :include, GroupedScope::Arish::Base