Skip to content

Commit

Permalink
Use relations in SelfGrouping.
Browse files Browse the repository at this point in the history
  • Loading branch information
metaskills committed Dec 6, 2011
1 parent 311ad5b commit 2bdfe3b
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 27 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@

= master

= 3.1.0 (unreleased)

* Works with ActiveRecord 3.1

* The group object is now an ActiveRecord::Relation so you can further scope it.

* New group.ids_sql which is an Arel SQL literal. Avoids large groups IDs and better query plans.


= 0.6.0 (May 06, 2009)

* ActiveRecord 2.3.14 compatibility.
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ http://metaskills.net/2008/09/28/jack-has_many-things/



## Installation & Usage
## Installation

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.

```ruby
gem 'grouped_scope', '~> 3.1.0'
```


## Setup

To use GroupedScope on a model it must have a `:group_id` column.

```ruby
Expand All @@ -30,6 +33,9 @@ class AddGroupId < ActiveRecord::Migration
end
```


## General Usage

Assume the following model.

```ruby
Expand Down Expand Up @@ -79,6 +85,30 @@ defined on the original association. For instance:
```


## Advanced Usage

The object returned by the `#group` method is an ActiveRecord relation on the targets class,
in this case `Employee`. Given this, you can further scope the grouped proxy if needed. Below,
we use the `:email_present` scope to refine the group down.

```ruby
class Employee < ActiveRecord::Base
has_many :reports
grouped_scope :reports
scope :email_present, where("email IS NOT NULL")
end

@employee_one = Employee.create :group_id => 5, :name => 'Ken'
@employee_two = Employee.create :group_id => 5, :name => 'MetaSkills', :email => '[email protected]'

# Only one employee is returned now.
@employee_one.group.email_present # => [#<Employee id: 1, group_id: 5, name: 'MetaSkills', email: '[email protected]']
```






## Todo List

Expand Down
4 changes: 2 additions & 2 deletions lib/grouped_scope/arish/associations/association_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class AssociationScope < ActiveRecord::Associations::AssociationScope
# in chunks, it would be easier to hook into. This more elegant version which supers
# up will only work for the has_many. https://gist.github.com/1434980
#
# We will just have to monitor rails everynow and then and update this. Thankfully this
# copy is only used in a group scope. FYI, our one line change is below.
# We will just have to monitor rails every now and then and update this. Thankfully this
# copy is only used in a group scope. FYI, our one line change is commented below.
def add_constraints(scope)
tables = construct_tables

Expand Down
65 changes: 47 additions & 18 deletions lib/grouped_scope/self_grouping.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
module GroupedScope
class SelfGroupping

attr_reader :proxy_owner
attr_reader :proxy_owner, :reflection

delegate :primary_key, :quote_value, :columns_hash, :to => :proxy_class
delegate :quote_value, :columns_hash, :to => :proxy_class

[].methods.each do |m|
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|respond_to\?)/
delegate m, :to => :group_proxy
unless m =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/
delegate m, :to => :grouped_proxy
end
end

Expand All @@ -18,22 +18,50 @@ def initialize(proxy_owner)
end

def ids
@ids ||= find_selves(group_id_scope_options).map(&:id)
grouped_scoped_ids.map(&primary_key.to_sym)
end

def ids_sql
Arel.sql(grouped_scoped_ids.to_sql)
end

# TODO: Note this.
def quoted_ids
ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
end

def with_reflection(reflection)
@reflection = reflection
yield
ensure
@reflection = nil
end

def respond_to?(method, include_private=false)
super || proxy_class.grouped_reflections[method].present?
super || proxy_class.grouped_reflections[method].present? || grouped_proxy.respond_to?(method, include_private)
end


protected

def group_proxy
@group_proxy ||= find_selves(group_scope_options)
def primary_key
reflection ? reflection.association_primary_key : proxy_class.primary_key
end

def arel_group_id
arel_table['group_id']
end

def arel_primary_key
arel_table[primary_key]
end

def arel_table
reflection ? Arel::Table.new(reflection.table_name) : proxy_class.arel_table
end

def grouped_proxy
@grouped_proxy ||= grouped_scoped
end

def grouped?
Expand All @@ -44,18 +72,13 @@ def all_grouped?
proxy_owner.all_grouped? rescue false
end

def find_selves(options={})
proxy_owner.class.find :all, options
end

def group_scope_options
return {} if all_grouped?
conditions = grouped? ? { :group_id => proxy_owner.group_id } : { primary_key => proxy_owner.id }
{ :conditions => conditions }
def grouped_scoped
return proxy_class.scoped if all_grouped?
proxy_class.where grouped? ? arel_group_id.eq(proxy_owner.group_id) : arel_primary_key.eq(proxy_owner.id)
end

def group_id_scope_options
{ :select => primary_key }.merge(group_scope_options)
def grouped_scoped_ids
grouped_scoped.select(arel_primary_key)
end

def proxy_class
Expand All @@ -72,6 +95,12 @@ def method_missing(method, *args)
else
proxy_owner.send(:"grouped_scope_#{method}", *args)
end
elsif grouped_proxy.respond_to?(method)
if block_given?
grouped_proxy.send(method, *args) { |*block_args| yield(*block_args) }
else
grouped_proxy.send(method, *args)
end
else
super
end
Expand Down
54 changes: 48 additions & 6 deletions test/grouped_scope/self_grouping_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase
@employee = FactoryGirl.create(:employee)
end

it 'return an array' do
assert_instance_of Array, @employee.group
end

it 'return #ids array' do
assert_equal [@employee.id], @employee.group.ids
e1 = FactoryGirl.create :employee, :group_id => 3
e2 = FactoryGirl.create :employee, :group_id => 3
assert_same_elements [e1.id, e2.id], e1.group.ids
end

it 'return #quoted_ids string for use in sql statments' do
Expand All @@ -35,6 +34,28 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase
end
end

describe 'for #with_reflection' do

before { @reflection = Employee.reflections[:reports] }

it 'will set a reflection and always set it back to nil' do
assert_nil @employee.group.reflection
@employee.group.with_reflection(@reflection) do
assert_equal @reflection, @employee.group.reflection
end
assert_nil @employee.group.reflection
end

it 'will use the primary key of the reflection' do
pk = 'association_primary_key'
@reflection.stubs :association_primary_key => pk
@employee.group.with_reflection(@reflection) do
assert_equal pk, @employee.group.send(:primary_key)
end
end

end

describe 'for Array delegates' do

it 'respond to first/last' do
Expand All @@ -56,8 +77,8 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase

describe 'Calling #group' do

it 'return an array' do
assert_instance_of Array, FactoryGirl.create(:employee).group
it 'returns a active record relation' do
assert_instance_of ActiveRecord::Relation, FactoryGirl.create(:employee).group
end

describe 'with a NIL group_id' do
Expand All @@ -74,6 +95,11 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase
assert @employee.group.include?(@employee)
end

it 'returns a sql literal for #ids_sql scoped to single record' do
@employee.group.ids_sql.must_be_instance_of Arel::Nodes::SqlLiteral
@employee.group.ids_sql.must_match %r{SELECT \"employees\".\"id\" FROM \"employees\" WHERE \"employees\".\"id\" = #{@employee.id}}
end

end

describe 'with a set group_id' do
Expand All @@ -90,6 +116,22 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase
assert @employee.group.include?(@employee)
end

it 'returns a sql literal for #ids_sql scoped to group' do
new_group_id = 420
e1 = FactoryGirl.create :employee, :group_id => new_group_id
e2 = FactoryGirl.create :employee, :group_id => new_group_id
e1.group.ids_sql.must_be_instance_of Arel::Nodes::SqlLiteral
e1.group.ids_sql.must_match %r{SELECT \"employees\".\"id\" FROM \"employees\" WHERE \"employees\".\"group_id\" = #{new_group_id}}
end

it 'allows the group to be further scoped' do
new_group_id = 420
e1 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Ken', :email => '[email protected]'
e2 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Hostmaster', :email => '[email protected]'
e3 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Ken', :email => '[email protected]'
assert_same_elements [e1, e2], e1.group.email_for_actionmoniker
end

end

describe 'with different groups available' do
Expand Down
1 change: 1 addition & 0 deletions test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class LegacyReport < ActiveRecord::Base
end

class Employee < ActiveRecord::Base
scope :email_for_actionmoniker, where("email LIKE '%@actionmoniker.com'")
has_many :reports do ; def urgent ; find(:all,:conditions => {:title => 'URGENT'}) ; end ; end
has_many :taxonomies, :as => :classable
has_many :department_memberships
Expand Down

0 comments on commit 2bdfe3b

Please sign in to comment.