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 +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
|