cohort_scope 0.1.5 → 0.2.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.
data/.gitignore CHANGED
@@ -21,3 +21,4 @@ pkg
21
21
  ## PROJECT::SPECIFIC
22
22
  Gemfile.lock
23
23
  test/test.log
24
+ *.gem
data/README.rdoc CHANGED
@@ -2,14 +2,18 @@
2
2
 
3
3
  Provides cohorts (in the form of ActiveRecord scopes) that dynamically widen until they contain a certain number of records.
4
4
 
5
- * <tt>big_cohort</tt> widens by finding the constraint that eliminates the most records and removing it.
6
- * <tt>strict_cohort</tt> widens by eliminating constraints in order.
5
+ * <tt>big_cohort</tt> widens by successively removing what it finds to be the most restrictive constraint until it reaches the minimum number of records
6
+ * <tt>strict_cohort</tt> widens by eliminating constraints in order until it reaches the minimum number of records
7
7
 
8
- = Real-world use
8
+ == Changes 0.1.x vs. 0.2.x
9
+
10
+ No longer "flattens" or "sanitizes" constraints by turning records into integer IDs, etc. You should pass in exactly what you would pass into a normal ActiveRecord relation/scope.
11
+
12
+ == Real-world use
9
13
 
10
14
  This has been at use at http://carbon.brighterplanet.com since April 2010, where it helps sift through climate data to come up with meaningful emissions calculations.
11
15
 
12
- = Quick start
16
+ == Quick start
13
17
 
14
18
  Let's pretend the U.S. Census provided information about birthday and favorite color:
15
19
 
@@ -28,20 +32,17 @@ Now I need to run a calculation that ideally uses birthday and favorite color, b
28
32
 
29
33
  What if my calculation privileges favorite color? In other words, if you can't give me a cohort of minimum size within the birthday constraint, at least give me one where everybody loves heliotrope:
30
34
 
31
- ordered_constraints = ActiveSupport::OrderedHash.new
32
- ordered_constraints[:favorite_color] = 'heliotrope'
33
- ordered_constraints[:birthdate] = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
35
+ ordered_constraints = []
36
+ ordered_constraints << [:favorite_color, 'heliotrope']
37
+ ordered_constraints << [:birthdate, (Date.parse('1980-01-01')..Date.parse('1990-01-01'))]
34
38
 
35
- Citizen.strict_cohort favorite_color_matters_most
39
+ Citizen.strict_cohort *favorite_color_matters_most
36
40
  # => [... a cohort of at least 1,000 records (otherwise it's empty),
37
41
  where everybody's favorite color IS heliotrope
38
42
  and everybody's birthday MAY be between 1980 and 1990 ...]
39
43
 
40
- = Wishlist
41
-
42
- * support for ruby 1.9's implicitly ordered hashes
43
- * support for constraining on <tt>IS NULL</tt> or <tt>IS NOT NULL</tt>
44
+ == Wishlist
44
45
 
45
46
  == Copyright
46
47
 
47
- Copyright (c) 2010 Seamus Abshere and Andy Rossmeissl. See LICENSE for details.
48
+ Copyright (c) 2011 Seamus Abshere and Andy Rossmeissl. See LICENSE for details.
data/cohort_scope.gemspec CHANGED
@@ -19,8 +19,9 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
- s.add_dependency "activesupport", "~> 3.0.0"
23
- s.add_dependency "activerecord", "~> 3.0.0"
24
- s.add_development_dependency "shoulda", ">= 2.10.3"
25
- s.add_development_dependency 'sqlite3-ruby'
22
+ s.add_dependency "activesupport", "~> 3.0"
23
+ s.add_dependency "activerecord", "~> 3.0"
24
+ s.add_development_dependency 'test-unit'
25
+ s.add_development_dependency 'mysql'
26
+ # s.add_development_dependency 'ruby-debug'
26
27
  end
data/lib/cohort_scope.rb CHANGED
@@ -1,44 +1,58 @@
1
1
  require 'active_record'
2
+
2
3
  require 'active_support'
3
4
  require 'active_support/version'
4
-
5
- require 'active_support/core_ext/module/delegation' if ActiveSupport::VERSION::MAJOR == 3
6
-
7
- require 'cohort_scope/version'
8
- require 'cohort_scope/cohort'
9
- require 'cohort_scope/big_cohort'
10
- require 'cohort_scope/strict_cohort'
5
+ if ActiveSupport::VERSION::MAJOR == 3
6
+ require 'active_support/json'
7
+ require 'active_support/core_ext/hash'
8
+ end
11
9
 
12
10
  module CohortScope
11
+ autoload :Cohort, 'cohort_scope/cohort'
12
+ autoload :BigCohort, 'cohort_scope/big_cohort'
13
+ autoload :StrictCohort, 'cohort_scope/strict_cohort'
14
+
13
15
  def self.extended(klass)
14
- klass.cattr_accessor :minimum_cohort_size, :instance_writer => false
16
+ klass.class_eval do
17
+ class << self
18
+ attr_accessor :minimum_cohort_size
19
+ end
20
+ end
15
21
  end
16
-
22
+
23
+ def self.conditions_for(constraints)
24
+ case constraints
25
+ when ::Array
26
+ constraints.inject({}) { |memo, (k, v)| memo[k] = v; memo }
27
+ when ::Hash
28
+ constraints.dup
29
+ end
30
+ end
31
+
17
32
  # Find the biggest scope possible by removing constraints <b>in any order</b>.
18
33
  # Returns an empty scope if it can't meet the minimum scope size.
19
- def big_cohort(constraints = {}, custom_minimum_cohort_size = self.minimum_cohort_size)
20
- raise ArgumentError, "You can't give a big_cohort an OrderedHash; do you want strict_cohort?" if constraints.is_a?(ActiveSupport::OrderedHash)
21
- BigCohort.create self, constraints, custom_minimum_cohort_size
34
+ def big_cohort(constraints, options = {})
35
+ BigCohort.create self, constraints, (options[:minimum_cohort_size] || minimum_cohort_size)
22
36
  end
23
37
 
24
38
  # Find the first acceptable scope by removing constraints <b>in strict order</b>, starting with the last constraint.
25
39
  # Returns an empty scope if it can't meet the minimum scope size.
26
40
  #
27
- # <tt>constraints</tt> must be an <tt>ActiveSupport::OrderedHash</tt> (no support for ruby 1.9's natively ordered hashes yet).
41
+ # <tt>constraints</tt> must be key/value pairs (splat if it's an array)
28
42
  #
29
43
  # Note that the first constraint is implicitly required.
30
44
  #
31
45
  # Take this example, where favorite color is considered to be "more important" than birthdate:
32
46
  #
33
- # ordered_constraints = ActiveSupport::OrderedHash.new
34
- # ordered_constraints[:favorite_color] = 'heliotrope'
35
- # ordered_constraints[:birthdate] = '1999-01-01'
36
- # Citizen.strict_cohort(ordered_constraints) #=> [...]
47
+ # ordered_constraints = [ [:favorite_color, 'heliotrope'], [:birthdate, '1999-01-01'] ]
48
+ # Citizen.strict_cohort(*ordered_constraints) #=> [...]
37
49
  #
38
50
  # If the original constraints don't meet the minimum scope size, then the only constraint that can be removed is birthdate.
39
51
  # In other words, this would never return a scope that was constrained on birthdate but not on favorite_color.
40
- def strict_cohort(constraints, custom_minimum_cohort_size = self.minimum_cohort_size)
41
- raise ArgumentError, "You need to give strict_cohort an OrderedHash" unless constraints.is_a?(ActiveSupport::OrderedHash)
42
- StrictCohort.create self, constraints, custom_minimum_cohort_size
52
+ def strict_cohort(*args)
53
+ args = args.dup
54
+ options = args.last.is_a?(::Hash) ? args.pop : {}
55
+ constraints = args
56
+ StrictCohort.create self, constraints, (options[:minimum_cohort_size] || minimum_cohort_size)
43
57
  end
44
58
  end
@@ -1,21 +1,14 @@
1
1
  module CohortScope
2
2
  class BigCohort < Cohort
3
-
4
3
  # Reduce constraints by removing them one by one and counting the results.
5
4
  #
6
5
  # The constraint whose removal leads to the highest record count is removed from the overall constraint set.
7
- def self.reduce_constraints(model, constraints)
8
- highest_count_after_removal = nil
9
- losing_key = nil
10
- constraints.keys.each do |key|
11
- test_constraints = constraints.except(key)
12
- count_after_removal = model.scoped.where(sanitize_constraints(model, test_constraints)).count
13
- if highest_count_after_removal.nil? or count_after_removal > highest_count_after_removal
14
- highest_count_after_removal = count_after_removal
15
- losing_key = key
16
- end
6
+ def self.reduce_constraints(active_record, constraints)
7
+ most_restrictive_constraint = constraints.keys.max_by do |key|
8
+ conditions = CohortScope.conditions_for constraints.except(key)
9
+ active_record.scoped.where(conditions).count
17
10
  end
18
- constraints.except losing_key
11
+ constraints.except most_restrictive_constraint
19
12
  end
20
13
  end
21
14
  end
@@ -1,80 +1,22 @@
1
1
  require 'delegate'
2
2
 
3
3
  module CohortScope
4
- class Cohort < Delegator
4
+ class Cohort < ::Delegator
5
5
 
6
6
  class << self
7
7
  # Recursively look for a scope that meets the constraints and is at least <tt>minimum_cohort_size</tt>.
8
- def create(model, constraints, custom_minimum_cohort_size)
9
- raise RuntimeError, "You need to set #{name}.minimum_cohort_size = X" unless model.minimum_cohort_size.present?
10
-
11
- if constraints.values.none? # failing base case
12
- empty_cohort = model.scoped.where '1 = 2'
13
- return new(empty_cohort)
8
+ def create(active_record, constraints, minimum_cohort_size)
9
+ if constraints.none? # failing base case
10
+ empty_scope = active_record.scoped.where '1 = 2'
11
+ return new(empty_scope)
14
12
  end
15
13
 
16
- constraint_hash = sanitize_constraints model, constraints
17
- constrained_scope = model.scoped.where(constraint_hash)
14
+ constrained_scope = active_record.scoped.where CohortScope.conditions_for(constraints)
18
15
 
19
- if constrained_scope.count >= custom_minimum_cohort_size
16
+ if constrained_scope.count >= minimum_cohort_size
20
17
  new constrained_scope
21
18
  else
22
- reduced_constraints = reduce_constraints(model, constraints)
23
- create(model, reduced_constraints, custom_minimum_cohort_size)
24
- end
25
- end
26
-
27
- # Sanitize constraints by
28
- # * removing nil constraints (so constraints like "X IS NULL" are impossible, sorry)
29
- # * converting ActiveRecord::Base objects into integer foreign key constraints
30
- def sanitize_constraints(model, constraints)
31
- new_hash = constraints.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash.new : Hash.new
32
- conditions = constraints.inject(new_hash) do |memo, tuple|
33
- k, v = tuple
34
- if v.kind_of?(ActiveRecord::Base)
35
- foreign_key = association_foreign_key model, k
36
- lookup_value = association_lookup_value model, k, v
37
- condition = { foreign_key => lookup_value }
38
- elsif !v.nil?
39
- condition = { k => v }
40
- end
41
- memo.merge! condition if condition.is_a? Hash
42
- memo
43
- end
44
- conditions
45
- end
46
-
47
- # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding primary keys.
48
- #
49
- # Only works for <tt>belongs_to</tt> relationships.
50
- #
51
- # For example, :car => <#Car> might get translated into :car_id => 44 or :car_type => 44 if :foreign_key option is given.
52
- def association_foreign_key(model, name)
53
- @association_foreign_key ||= {}
54
- return @association_foreign_key[name] if @association_foreign_key.has_key? name
55
- association = model.reflect_on_association name
56
- raise "there is no association #{name.inspect} on #{model}" if association.nil?
57
- raise "can't use cohort scope on :through associations (#{self.name} #{name})" if association.options.has_key? :through
58
- foreign_key = association.instance_variable_get(:@options)[:foreign_key]
59
- if !foreign_key.blank?
60
- @association_foreign_key[name] = foreign_key
61
- else
62
- @association_foreign_key[name] = association.primary_key_name
63
- end
64
- end
65
-
66
- # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding lookup values
67
- #
68
- # Only works for <tt>belongs_to</tt> relationships.
69
- #
70
- # For example, :car => <#Car> might get translated into :car_id => 44 or :car_id => 'JHK123' if :primary_key option is given.
71
- def association_lookup_value(model, name, value)
72
- association = model.reflect_on_association name
73
- primary_key = association.instance_variable_get(:@options)[:primary_key]
74
- if primary_key.blank?
75
- value.to_param
76
- else
77
- value.send primary_key
19
+ create active_record, reduce_constraints(active_record, constraints), minimum_cohort_size
78
20
  end
79
21
  end
80
22
  end
@@ -109,7 +51,7 @@ module CohortScope
109
51
  end
110
52
 
111
53
  def inspect
112
- "<Massive ActiveRecord scope with #{count} members>"
54
+ "<Cohort scope with #{count} members>"
113
55
  end
114
56
  end
115
57
  end
@@ -1,13 +1,8 @@
1
1
  module CohortScope
2
2
  class StrictCohort < Cohort
3
-
4
- # (Used by <tt>strict_cohort</tt>)
5
- #
6
3
  # Reduce constraints by removing the least important one.
7
- def self.reduce_constraints(model, constraints)
8
- reduced_constraints = constraints.dup
9
- reduced_constraints.delete constraints.keys.last
10
- reduced_constraints
4
+ def self.reduce_constraints(active_record, constraints)
5
+ constraints[0..-2]
11
6
  end
12
7
  end
13
8
  end
@@ -1,3 +1,3 @@
1
1
  module CohortScope
2
- VERSION = '0.1.5'
2
+ VERSION = '0.2.0'
3
3
  end
data/test/helper.rb CHANGED
@@ -2,8 +2,6 @@ require 'rubygems'
2
2
  require 'bundler'
3
3
  Bundler.setup
4
4
  require 'test/unit'
5
- require 'shoulda'
6
- require 'logger'
7
5
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
7
  require 'cohort_scope'
@@ -11,36 +9,39 @@ require 'cohort_scope'
11
9
  class Test::Unit::TestCase
12
10
  end
13
11
 
14
- $logger = Logger.new 'test/test.log' #STDOUT
15
- ActiveSupport::Notifications.subscribe do |*args|
16
- event = ActiveSupport::Notifications::Event.new(*args)
17
- $logger.debug "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
18
- end
12
+ # require 'logger'
13
+ # ActiveRecord::Base.logger = Logger.new($stderr)
19
14
 
20
15
  ActiveRecord::Base.establish_connection(
21
- 'adapter' => 'sqlite3',
22
- 'database' => ':memory:'
16
+ 'adapter' => 'mysql',
17
+ 'database' => 'test_cohort_scope',
18
+ 'username' => 'root',
19
+ 'password' => 'password'
23
20
  )
24
21
 
25
- ActiveRecord::Schema.define(:version => 20090819143429) do
26
- create_table 'citizens', :force => true do |t|
27
- t.date 'birthdate'
28
- t.string 'favorite_color'
29
- t.integer 'teeth'
30
- end
31
- create_table 'houses', :force => true do |t|
32
- t.string 'period'
33
- t.string 'address'
34
- t.integer 'storeys'
35
- end
36
- create_table 'styles', :force => true do |t|
37
- t.string 'period'
38
- t.string 'name'
39
- end
40
- create_table 'residents', :force => true do |t|
41
- t.integer 'house_id'
42
- t.string 'name'
43
- end
22
+ c = ActiveRecord::Base.connection
23
+ c.create_table 'citizens', :force => true do |t|
24
+ t.date 'birthdate'
25
+ t.string 'favorite_color'
26
+ t.integer 'teeth'
27
+ end
28
+ c.create_table 'houses', :force => true do |t|
29
+ t.string 'period_id'
30
+ t.string 'address'
31
+ t.integer 'storeys'
32
+ end
33
+ c.create_table 'periods', :force => true, :id => false do |t|
34
+ t.string 'name'
35
+ end
36
+ c.execute "ALTER TABLE periods ADD PRIMARY KEY (name)"
37
+ c.create_table 'styles', :force => true, :id => false do |t|
38
+ t.string 'name'
39
+ t.string 'period_id'
40
+ end
41
+ c.execute "ALTER TABLE styles ADD PRIMARY KEY (name)"
42
+ c.create_table 'residents', :force => true do |t|
43
+ t.integer 'house_id'
44
+ t.string 'name'
44
45
  end
45
46
 
46
47
  class Citizen < ActiveRecord::Base
@@ -66,27 +67,46 @@ end
66
67
  Citizen.create! :birthdate => birthdate, :favorite_color => favorite_color, :teeth => teeth
67
68
  end
68
69
 
70
+ class Period < ActiveRecord::Base
71
+ set_primary_key :name
72
+ has_many :styles
73
+ has_many :houses
74
+
75
+ # hack to make sure rails doesn't protect the foreign key columns
76
+ self._protected_attributes = BlackList.new
77
+ end
78
+
69
79
  class Style < ActiveRecord::Base
80
+ set_primary_key :name
70
81
  extend CohortScope
71
82
  self.minimum_cohort_size = 3
72
- has_many :houses
83
+ belongs_to :period
84
+ has_many :houses, :through => :period, :foreign_key => 'name'
85
+
86
+ # hack to make sure rails doesn't protect the foreign key columns
87
+ self._protected_attributes = BlackList.new
73
88
  end
89
+
74
90
  class House < ActiveRecord::Base
75
- belongs_to :style, :foreign_key => 'period', :primary_key => 'period'
91
+ belongs_to :period
92
+ has_many :styles, :through => :period
76
93
  has_one :resident
77
94
  end
95
+
78
96
  class Resident < ActiveRecord::Base
79
- has_one :house
97
+ belongs_to :house
80
98
  end
81
99
 
82
- Style.create! :period => 'arts and crafts', :name => 'classical revival'
83
- Style.create! :period => 'arts and crafts', :name => 'gothic'
84
- Style.create! :period => 'arts and crafts', :name => 'art deco'
85
- Style.create! :period => 'victorian', :name => 'stick-eastlake'
86
- Style.create! :period => 'victorian', :name => 'queen anne'
87
- h1 = House.create! :period => 'arts and crafts', :address => '123 Maple', :storeys => 1
88
- h2 = House.create! :period => 'arts and crafts', :address => '223 Walnut', :storeys => 2
89
- h3 = House.create! :period => 'victorian', :address => '323 Pine', :storeys => 2
100
+ p1 = Period.create! :name => 'arts and crafts'
101
+ p2 = Period.create! :name => 'victorian'
102
+ Style.create! :period => p1, :name => 'classical revival'
103
+ Style.create! :period => p1, :name => 'gothic'
104
+ Style.create! :period => p1, :name => 'art deco'
105
+ Style.create! :period => p2, :name => 'stick-eastlake'
106
+ Style.create! :period => p2, :name => 'queen anne'
107
+ h1 = House.create! :period => p1, :address => '123 Maple', :storeys => 1
108
+ h2 = House.create! :period => p1, :address => '223 Walnut', :storeys => 2
109
+ h3 = House.create! :period => p2, :address => '323 Pine', :storeys => 2
90
110
  Resident.create! :house => h1, :name => 'Bob'
91
111
  Resident.create! :house => h2, :name => 'Rob'
92
112
  Resident.create! :house => h3, :name => 'Gob'
@@ -0,0 +1,31 @@
1
+ require 'helper'
2
+
3
+ class TestBigCohort < Test::Unit::TestCase
4
+ def setup
5
+ Citizen.minimum_cohort_size = 3
6
+ @date_range = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
7
+ end
8
+
9
+ def test_001_empty
10
+ cohort = Citizen.big_cohort :favorite_color => 'heliotrope'
11
+ assert_equal 0, cohort.count
12
+ end
13
+
14
+ def test_002_optional_minimum_cohort_size_at_runtime
15
+ cohort = Citizen.big_cohort({:favorite_color => 'heliotrope'}, :minimum_cohort_size => 0)
16
+ assert_equal 1, cohort.count
17
+ end
18
+
19
+ def test_003_seek_cohort_of_maximum_size
20
+ cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
21
+ assert_equal 9, cohort.count
22
+ assert cohort.any? { |m| m.favorite_color != 'heliotrope' }
23
+ assert cohort.all? { |m| @date_range.include? m.birthdate }
24
+ end
25
+
26
+ def test_004_unsurprising_treatment_of_arrays
27
+ assert_equal 3, Citizen.big_cohort({:favorite_color => 'blue'}, :minimum_cohort_size => 0).count
28
+ assert_equal 1, Citizen.big_cohort({:favorite_color => 'heliotrope'}, :minimum_cohort_size => 0).count
29
+ assert_equal 4, Citizen.big_cohort({:favorite_color => ['heliotrope', 'blue']}, :minimum_cohort_size => 0).count
30
+ end
31
+ end
@@ -6,14 +6,41 @@ class TestCohortScope < Test::Unit::TestCase
6
6
  @date_range = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
7
7
  end
8
8
 
9
- should "properly use blocks" do
9
+ def test_001_has_sane_associations
10
+ assert Period.first.styles.first
11
+ assert Period.first.houses.first
12
+ assert Style.first.period
13
+ assert Style.first.houses.first
14
+ assert House.first.period
15
+ assert House.first.styles.first
16
+ assert House.all.any? { |h| h.resident }
17
+ assert House.joins(:styles).where(:styles => { :name => [style] }).first.styles.include?(style)
18
+ assert Style.joins(:houses).where(:houses => { :id => [house1] }).first
19
+ end
20
+
21
+ # confusing as hell because houses have styles according to periods, which is not accurate
22
+ def test_002a_complicated_cohorts_with_joins
23
+ assert_equal 3, Style.joins(:houses).big_cohort(:houses => { :id => [house1]}).length
24
+ assert_equal 3, Style.joins(:houses).big_cohort(:houses => { :id => [house1]}, :name => 'foooooooo').length
25
+ # these return 2, which is too small
26
+ assert_equal 0, Style.joins(:houses).big_cohort(:houses => { :id => [house3]}).length
27
+ assert_equal 0, Style.joins(:houses).big_cohort(:houses => { :id => [house3]}, :name => 'classical revival').length
28
+ end
29
+
30
+ # should this even work in theory?
31
+ def test_002b_simplified_joins
32
+ flunk
33
+ assert_equal 3, Style.big_cohort(:houses => [house1]).length
34
+ end
35
+
36
+ def test_003_redefine_any_query_method
10
37
  cohort = Citizen.big_cohort(:birthdate => @date_range)
11
38
  assert cohort.all? { |c| true }
12
39
  assert cohort.any? { |c| true }
13
40
  assert !cohort.none? { |c| true }
14
41
  end
15
-
16
- should "actually run blocks" do
42
+
43
+ def test_004_really_run_blocks
17
44
  assert_raises(RuntimeError, 'A') do
18
45
  Citizen.big_cohort(:birthdate => @date_range).all? { |c| raise 'A' }
19
46
  end
@@ -24,110 +51,49 @@ class TestCohortScope < Test::Unit::TestCase
24
51
  Citizen.big_cohort(:birthdate => @date_range).none? { |c| raise 'C' }
25
52
  end
26
53
  end
27
-
28
- should "only show the count in the json representation" do
54
+
55
+ def test_005_short_to_json
29
56
  cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
30
57
  assert_equal({ :members => 9 }.to_json, cohort.to_json)
31
58
  end
32
59
 
33
- should "not mess with normal as_json" do
60
+ def test_006_doesnt_mess_with_active_record_json
34
61
  non_cohort = Citizen.all
35
62
  assert_equal non_cohort.to_a.as_json, non_cohort.as_json
36
63
  end
37
64
 
38
- should "not mess with normal inspect" do
65
+ def test_007_doesnt_mess_with_active_record_inspect
39
66
  non_cohort = Citizen.all
40
67
  assert_equal non_cohort.to_a.inspect, non_cohort.inspect
41
68
  end
42
69
 
43
- should "inspect to a short string" do
70
+ def test_008_short_inspect
44
71
  cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
45
- assert_equal "<Massive ActiveRecord scope with 9 members>", cohort.inspect
72
+ assert_equal "<Cohort scope with 9 members>", cohort.inspect
46
73
  end
47
74
 
48
- should "not get fooled into revealing all of its members by a parent's #to_hash" do
75
+ def test_009_not_reveal_itself_in_to_hash
49
76
  cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
50
77
  assert_equal '{"c":{"members":9}}', { :c => cohort }.to_hash.to_json
51
78
  end
52
-
53
- should "retain the scope's original behavior" do
79
+
80
+ def test_010_work_as_delegator
54
81
  cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
55
82
  assert_kind_of Citizen, cohort.last
56
83
  assert_kind_of Citizen, cohort.where(:teeth => 31).first
57
84
  end
58
-
59
- should "raise if no minimum_cohort_size is specified" do
60
- Citizen.minimum_cohort_size = nil
61
- assert_raises(RuntimeError) {
62
- Citizen.big_cohort Hash.new
63
- }
64
- assert_raises(RuntimeError) {
65
- Citizen.strict_cohort ActiveSupport::OrderedHash.new
66
- }
67
- end
68
-
69
- context "big_cohort" do
70
- should "return an empty cohort if it can't find one that meets size requirements" do
71
- cohort = Citizen.big_cohort :favorite_color => 'heliotrope'
72
- assert_equal 0, cohort.count
73
- end
74
85
 
75
- should "take minimum_cohort_size as an optional argument" do
76
- cohort = Citizen.big_cohort({:favorite_color => 'heliotrope'}, 0)
77
- assert_equal 1, cohort.count
78
- end
86
+ private
79
87
 
80
- should "seek a cohort of maximum size" do
81
- cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
82
- assert_equal 9, cohort.count
83
- assert cohort.any? { |m| m.favorite_color != 'heliotrope' }
84
- assert cohort.all? { |m| @date_range.include? m.birthdate }
85
- end
86
-
87
- should "raise if an OrderedHash is given to big_cohort" do
88
- assert_raises(ArgumentError) {
89
- Citizen.big_cohort ActiveSupport::OrderedHash.new
90
- }
91
- end
88
+ def style
89
+ @style ||= Style.find 'classical revival'
92
90
  end
93
91
 
94
- context "strict_cohort" do
95
- should "raise if a non-OrderedHash is given to strict_cohort" do
96
- assert_raises(ArgumentError) {
97
- Citizen.strict_cohort Hash.new
98
- }
99
- end
100
-
101
- should "take minimum_cohort_size as an optional argument" do
102
- ordered_attributes = ActiveSupport::OrderedHash.new
103
- ordered_attributes[:favorite_color] = 'heliotrope'
104
-
105
- cohort = Citizen.strict_cohort ordered_attributes, 0
106
- assert_equal 1, cohort.count
107
- end
108
-
109
- should "return an empty cohort if it can't find one that meets size requirements" do
110
- ordered_attributes = ActiveSupport::OrderedHash.new
111
- ordered_attributes[:favorite_color] = 'heliotrope'
112
-
113
- cohort = Citizen.strict_cohort ordered_attributes
114
- assert_equal 0, cohort.count
115
- end
92
+ def house1
93
+ @house1 ||= House.find 2
94
+ end
116
95
 
117
- should "seek a cohort by discarding attributes in order" do
118
- favorite_color_matters_most = ActiveSupport::OrderedHash.new
119
- favorite_color_matters_most[:favorite_color] = 'heliotrope'
120
- favorite_color_matters_most[:birthdate] = @date_range
121
-
122
- birthdate_matters_most = ActiveSupport::OrderedHash.new
123
- birthdate_matters_most[:birthdate] = @date_range
124
- birthdate_matters_most[:favorite_color] = 'heliotrope'
125
-
126
- cohort = Citizen.strict_cohort favorite_color_matters_most
127
- assert_equal 0, cohort.count
128
-
129
- cohort = Citizen.strict_cohort birthdate_matters_most
130
- assert_equal 9, cohort.count
131
- end
96
+ def house3
97
+ @house3 ||= House.find 3
132
98
  end
133
99
  end
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class TestStrictCohort < Test::Unit::TestCase
4
+ def setup
5
+ Citizen.minimum_cohort_size = 3
6
+ @date_range = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
7
+ end
8
+
9
+ def test_001_empty
10
+ cohort = Citizen.strict_cohort
11
+ assert_equal 0, cohort.count
12
+ end
13
+
14
+ def test_002_optional_minimum_cohort_size_at_runtime
15
+ cohort = Citizen.strict_cohort [:favorite_color, 'heliotrope'], :minimum_cohort_size => 0
16
+ assert_equal 1, cohort.count
17
+ end
18
+
19
+ def test_003_seek_cohort_by_discarding_constraints_in_order
20
+ favorite_color_matters_most = [ [:favorite_color, 'heliotrope'], [:birthdate, @date_range] ]
21
+ birthdate_matters_most = [ [:birthdate, @date_range], [:favorite_color, 'heliotrope'] ]
22
+
23
+ cohort = Citizen.strict_cohort *favorite_color_matters_most
24
+ assert_equal 0, cohort.count
25
+
26
+ cohort = Citizen.strict_cohort *birthdate_matters_most
27
+ assert_equal 9, cohort.count
28
+ end
29
+ end
metadata CHANGED
@@ -1,12 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cohort_scope
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 1
8
- - 5
9
- version: 0.1.5
4
+ prerelease:
5
+ version: 0.2.0
10
6
  platform: ruby
11
7
  authors:
12
8
  - Seamus Abshere
@@ -16,7 +12,7 @@ autorequire:
16
12
  bindir: bin
17
13
  cert_chain: []
18
14
 
19
- date: 2011-03-04 00:00:00 -05:00
15
+ date: 2011-05-17 00:00:00 -05:00
20
16
  default_executable:
21
17
  dependencies:
22
18
  - !ruby/object:Gem::Dependency
@@ -27,11 +23,7 @@ dependencies:
27
23
  requirements:
28
24
  - - ~>
29
25
  - !ruby/object:Gem::Version
30
- segments:
31
- - 3
32
- - 0
33
- - 0
34
- version: 3.0.0
26
+ version: "3.0"
35
27
  type: :runtime
36
28
  version_requirements: *id001
37
29
  - !ruby/object:Gem::Dependency
@@ -42,38 +34,28 @@ dependencies:
42
34
  requirements:
43
35
  - - ~>
44
36
  - !ruby/object:Gem::Version
45
- segments:
46
- - 3
47
- - 0
48
- - 0
49
- version: 3.0.0
37
+ version: "3.0"
50
38
  type: :runtime
51
39
  version_requirements: *id002
52
40
  - !ruby/object:Gem::Dependency
53
- name: shoulda
41
+ name: test-unit
54
42
  prerelease: false
55
43
  requirement: &id003 !ruby/object:Gem::Requirement
56
44
  none: false
57
45
  requirements:
58
46
  - - ">="
59
47
  - !ruby/object:Gem::Version
60
- segments:
61
- - 2
62
- - 10
63
- - 3
64
- version: 2.10.3
48
+ version: "0"
65
49
  type: :development
66
50
  version_requirements: *id003
67
51
  - !ruby/object:Gem::Dependency
68
- name: sqlite3-ruby
52
+ name: mysql
69
53
  prerelease: false
70
54
  requirement: &id004 !ruby/object:Gem::Requirement
71
55
  none: false
72
56
  requirements:
73
57
  - - ">="
74
58
  - !ruby/object:Gem::Version
75
- segments:
76
- - 0
77
59
  version: "0"
78
60
  type: :development
79
61
  version_requirements: *id004
@@ -100,8 +82,9 @@ files:
100
82
  - lib/cohort_scope/strict_cohort.rb
101
83
  - lib/cohort_scope/version.rb
102
84
  - test/helper.rb
103
- - test/test_cohort.rb
85
+ - test/test_big_cohort.rb
104
86
  - test/test_cohort_scope.rb
87
+ - test/test_strict_cohort.rb
105
88
  has_rdoc: true
106
89
  homepage: https://github.com/seamusabshere/cohort_scope
107
90
  licenses: []
@@ -116,25 +99,22 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
99
  requirements:
117
100
  - - ">="
118
101
  - !ruby/object:Gem::Version
119
- segments:
120
- - 0
121
102
  version: "0"
122
103
  required_rubygems_version: !ruby/object:Gem::Requirement
123
104
  none: false
124
105
  requirements:
125
106
  - - ">="
126
107
  - !ruby/object:Gem::Version
127
- segments:
128
- - 0
129
108
  version: "0"
130
109
  requirements: []
131
110
 
132
111
  rubyforge_project: cohort_scope
133
- rubygems_version: 1.3.7
112
+ rubygems_version: 1.6.2
134
113
  signing_key:
135
114
  specification_version: 3
136
115
  summary: Provides cohorts (in the form of ActiveRecord scopes) that dynamically widen until they contain a certain number of records.
137
116
  test_files:
138
117
  - test/helper.rb
139
- - test/test_cohort.rb
118
+ - test/test_big_cohort.rb
140
119
  - test/test_cohort_scope.rb
120
+ - test/test_strict_cohort.rb
data/test/test_cohort.rb DELETED
@@ -1,57 +0,0 @@
1
- require 'helper'
2
-
3
- class TestCohort < Test::Unit::TestCase
4
- def setup
5
- Citizen.minimum_cohort_size = 3
6
- @date_range = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
7
- end
8
-
9
- def style
10
- @style ||= Style.find_by_period 'arts and crafts'
11
- end
12
-
13
- context '.sanitize_constraints' do
14
- should 'remove nil constraints' do
15
- constraints = CohortScope::Cohort.sanitize_constraints Style, :eh => :tu, :bru => :te, :caesar => nil
16
- assert_does_not_contain constraints.keys, :caesar
17
- end
18
- should 'keep normal constraints' do
19
- constraints = CohortScope::Cohort.sanitize_constraints Style, :eh => :tu, :bru => :te, :caesar => nil
20
- assert_equal :tu, constraints[:eh]
21
- end
22
- should 'include constraints that are models' do
23
- gob = Resident.find_by_name 'Gob'
24
- constraints = CohortScope::Cohort.sanitize_constraints House, :resident => gob
25
- assert_equal gob.house_id, constraints[:house_id]
26
- end
27
- should 'include constraints that are models not related by primary key' do
28
- constraints = CohortScope::Cohort.sanitize_constraints House, :style => style
29
- assert_equal 'arts and crafts', constraints['period']
30
- end
31
- end
32
-
33
- context '.association_foreign_key' do
34
- should 'include constraints that are models related by a standard foreign key' do
35
- gob = Resident.find_by_name('Gob')
36
- key = CohortScope::Cohort.association_foreign_key Resident, :house
37
- assert_equal 'resident_id', key
38
- end
39
- should 'include constraints that are models related by a non-standard foreign key' do
40
- key = CohortScope::Cohort.association_foreign_key House, :style
41
- assert_equal 'period', key
42
- end
43
- end
44
-
45
- context '.association_lookup_value' do
46
- should 'include constraints that are models related by a standard foreign key' do
47
- gob = Resident.find_by_name('Gob')
48
- lookup = CohortScope::Cohort.association_lookup_value Resident, :house, gob
49
- assert_equal gob.to_param, lookup
50
- end
51
- should 'include constraints that are models related by a non-standard foreign key key' do
52
- rev = Style.find_by_name 'classical revival'
53
- lookup = CohortScope::Cohort.association_lookup_value House, :style, rev
54
- assert_equal 'arts and crafts', lookup
55
- end
56
- end
57
- end