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 +1 -0
- data/README.rdoc +14 -13
- data/cohort_scope.gemspec +5 -4
- data/lib/cohort_scope.rb +34 -20
- data/lib/cohort_scope/big_cohort.rb +5 -12
- data/lib/cohort_scope/cohort.rb +9 -67
- data/lib/cohort_scope/strict_cohort.rb +2 -7
- data/lib/cohort_scope/version.rb +1 -1
- data/test/helper.rb +59 -39
- data/test/test_big_cohort.rb +31 -0
- data/test/test_cohort_scope.rb +47 -81
- data/test/test_strict_cohort.rb +29 -0
- metadata +13 -33
- data/test/test_cohort.rb +0 -57
data/.gitignore
CHANGED
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
|
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
|
-
|
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
|
-
|
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 =
|
32
|
-
ordered_constraints[:favorite_color
|
33
|
-
ordered_constraints[:birthdate
|
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
|
-
|
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)
|
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
|
23
|
-
s.add_dependency "activerecord", "~> 3.0
|
24
|
-
s.add_development_dependency
|
25
|
-
s.add_development_dependency '
|
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/
|
6
|
-
|
7
|
-
|
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.
|
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 = {}
|
20
|
-
|
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
|
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 =
|
34
|
-
# ordered_constraints[
|
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(
|
41
|
-
|
42
|
-
|
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(
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
11
|
+
constraints.except most_restrictive_constraint
|
19
12
|
end
|
20
13
|
end
|
21
14
|
end
|
data/lib/cohort_scope/cohort.rb
CHANGED
@@ -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(
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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 >=
|
16
|
+
if constrained_scope.count >= minimum_cohort_size
|
20
17
|
new constrained_scope
|
21
18
|
else
|
22
|
-
|
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
|
-
"<
|
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(
|
8
|
-
|
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
|
data/lib/cohort_scope/version.rb
CHANGED
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
|
-
|
15
|
-
|
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' => '
|
22
|
-
'database' => '
|
16
|
+
'adapter' => 'mysql',
|
17
|
+
'database' => 'test_cohort_scope',
|
18
|
+
'username' => 'root',
|
19
|
+
'password' => 'password'
|
23
20
|
)
|
24
21
|
|
25
|
-
ActiveRecord::
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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 :
|
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
|
-
|
97
|
+
belongs_to :house
|
80
98
|
end
|
81
99
|
|
82
|
-
|
83
|
-
|
84
|
-
Style.create! :period =>
|
85
|
-
Style.create! :period =>
|
86
|
-
Style.create! :period =>
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
data/test/test_cohort_scope.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
70
|
+
def test_008_short_inspect
|
44
71
|
cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
|
45
|
-
assert_equal "<
|
72
|
+
assert_equal "<Cohort scope with 9 members>", cohort.inspect
|
46
73
|
end
|
47
74
|
|
48
|
-
|
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
|
-
|
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
|
-
|
76
|
-
cohort = Citizen.big_cohort({:favorite_color => 'heliotrope'}, 0)
|
77
|
-
assert_equal 1, cohort.count
|
78
|
-
end
|
86
|
+
private
|
79
87
|
|
80
|
-
|
81
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
118
|
-
|
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:
|
5
|
-
|
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-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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:
|
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/
|
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.
|
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/
|
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
|