require 'delegate'
module CohortScope
class Cohort < Delegator
class << self
# Recursively look for a scope that meets the constraints and is at least minimum_cohort_size.
def create(model, constraints, custom_minimum_cohort_size)
raise RuntimeError, "You need to set #{name}.minimum_cohort_size = X" unless model.minimum_cohort_size.present?
if constraints.values.none? # failing base case
empty_cohort = model.scoped.where '1 = 2'
return new(empty_cohort)
end
constraint_hash = sanitize_constraints model, constraints
constrained_scope = model.scoped.where(constraint_hash)
if constrained_scope.count >= custom_minimum_cohort_size
new constrained_scope
else
reduced_constraints = reduce_constraints(model, constraints)
create(model, reduced_constraints, custom_minimum_cohort_size)
end
end
# Sanitize constraints by
# * removing nil constraints (so constraints like "X IS NULL" are impossible, sorry)
# * converting ActiveRecord::Base objects into integer foreign key constraints
def sanitize_constraints(model, constraints)
new_hash = constraints.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash.new : Hash.new
conditions = constraints.inject(new_hash) do |memo, tuple|
k, v = tuple
if v.kind_of?(ActiveRecord::Base)
primary_key = association_primary_key(model, k)
param = v.respond_to?(primary_key) ? v.send(primary_key) : v.to_param
condition = { primary_key => param }
elsif !v.nil?
condition = { k => v }
end
memo.merge! condition if condition.is_a? Hash
memo
end
conditions
end
# Convert constraints that are provided as ActiveRecord::Base objects into their corresponding integer primary keys.
#
# Only works for belongs_to relationships.
#
# For example, :car => <#Car> might get translated into :car_id => 44.
def association_primary_key(model, name)
@_cohort_association_primary_keys ||= {}
return @_cohort_association_primary_keys[name] if @_cohort_association_primary_keys.has_key? name
a = model.reflect_on_association name
raise "there is no association #{name.inspect} on #{model}" if a.nil?
raise "can't use cohort scope on :through associations (#{self.name} #{name})" if a.options.has_key? :through
if !a.primary_key_name.blank?
@_cohort_association_primary_keys[name] = a.primary_key_name
else
raise "we need some other way to find primary key"
end
end
end
def initialize(obj)
@_ch_obj = obj
end
def __getobj__
@_ch_obj
end
def as_json(*)
{ :members => count }
end
def inspect
""
end
end
end