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) foreign_key = association_foreign_key model, k lookup_value = association_lookup_value model, k, v condition = { foreign_key => lookup_value } 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 primary keys. # # Only works for belongs_to relationships. # # For example, :car => <#Car> might get translated into :car_id => 44 or :car_type => 44 if :foreign_key option is given. def association_foreign_key(model, name) @association_foreign_key ||= {} return @association_foreign_key[name] if @association_foreign_key.has_key? name association = model.reflect_on_association name raise "there is no association #{name.inspect} on #{model}" if association.nil? raise "can't use cohort scope on :through associations (#{self.name} #{name})" if association.options.has_key? :through foreign_key = association.instance_variable_get(:@options)[:foreign_key] if !foreign_key.blank? @association_foreign_key[name] = foreign_key else @association_foreign_key[name] = association.primary_key_name end end # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding lookup values # # Only works for belongs_to relationships. # # For example, :car => <#Car> might get translated into :car_id => 44 or :car_id => 'JHK123' if :primary_key option is given. def association_lookup_value(model, name, value) association = model.reflect_on_association name primary_key = association.instance_variable_get(:@options)[:primary_key] if primary_key.blank? value.to_param else value.send primary_key end end end def initialize(obj) super @_ch_obj = obj end def __getobj__ @_ch_obj end def __setobj__(obj) @_ch_obj = obj end # sabshere 2/1/11 overriding as_json per usual doesn't seem to work def to_json(*) { :members => count }.to_json end # sabshere 2/1/11 ActiveRecord does this for #any? but not for #none? def none?(&blk) if block_given? to_a.none? &blk else super end end def inspect "" end end end