cohort_scope 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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