module Statistics class << self def included(base) base.extend(HasStats) end def default_filters(filters) ActiveRecord::Base.instance_eval { @filter_all_on = filters } end def supported_calculations [:average, :count, :maximum, :minimum, :sum] end end # This extension provides the ability to define statistics for reporting purposes module HasStats # OPTIONS: # #* +average+, +count+, +sum+, +maximum+, +minimum+ - Only one of these keys is passed, which # one depends on the type of operation. The value is an array of named scopes to scope the # operation by (+:all+ should be used if no scopes are to be applied) #* +column_name+ - The SQL column to perform the operation on (default: +id+) #* +filter_on+ - A hash with keys that represent filters. The with values in the has are rules # on how to generate the query for the correspond filter. #* +cached_for+ - A duration for how long to cache this specific statistic # # Additional options can also be passed in that would normally be passed to an ActiveRecord # +calculate+ call, like +conditions+, +joins+, etc # # EXAMPLE: # # class MockModel < ActiveRecord::Base # # named_scope :my_scope, :conditions => 'value > 5' # # define_statistic "Basic Count", :count => :all # define_statistic "Basic Sum", :sum => :all, :column_name => 'amount' # define_statistic "Chained Scope Count", :count => [:all, :my_scope] # define_statistic "Default Filter", :count => :all # define_statistic "Custom Filter", :count => :all, :filter_on => { :channel => 'channel = ?', :start_date => 'DATE(created_at) > ?' } # define_statistic "Cached", :count => :all, :filter_on => { :channel => 'channel = ?', :blah => 'blah = ?' }, :cache_for => 1.second # end def define_statistic(name, options) method_name = name.to_s.gsub(" ", "").underscore + "_stat" @statistics ||= {} @filter_all_on ||= ActiveRecord::Base.instance_eval { @filter_all_on } @statistics[name] = method_name options = { :column_name => :id }.merge(options) calculation = options.keys.find {|opt| Statistics::supported_calculations.include?(opt)} calculation ||= :count # We must use the metaclass here to metaprogrammatically define a class method (class< options[:cache_for]) if options[:cache_for] stat_value end end end # Defines a statistic using a block that has access to all other defined statistics # # EXAMPLE: # class MockModel < ActiveRecord::Base # define_statistic "Basic Count", :count => :all # define_statistic "Basic Sum", :sum => :all, :column_name => 'amount' # define_calculated_statistic "Total Profit" # defined_stats('Basic Sum') * defined_stats('Basic Count') # end def define_calculated_statistic(name, &block) method_name = name.to_s.gsub(" ", "").underscore + "_stat" @statistics ||= {} @statistics[name] = method_name (class< 'registered', :user_status => 'active') def statistics(filters = {}, except = nil) (@statistics || {}).inject({}) do |stats_hash, stat| stats_hash[stat.first] = send(stat.last, filters) if stat.last != except stats_hash end end # returns a single statistic based on the +stat_name+ paramater passed in and # similarly to the +statistics+ method, it also can take filters. # # EXAMPLE: # MockModel.get_stat('Basic Count') # MockModel.get_stat('Basic Count', :user_type => 'registered', :user_status => 'active') def get_stat(stat_name, filters = {}) send(@statistics[stat_name], filters) if @statistics[stat_name] end # to keep things DRY anything that all statistics need to be filterable by can be defined # seperatly using this method # # EXAMPLE: # # class MockModel < ActiveRecord::Base # define_statistic "Basic Count", :count => :all # define_statistic "Basic Sum", :sum => :all, :column_name => 'amount' # # filter_all_stats_on(:user_id, "user_id = ?") # end def filter_all_stats_on(name, cond) @filter_all_on ||= {} @filter_all_on[name] = cond end private def defined_stats(name) get_stat(name, @filters) end def sql_options(options) Statistics::supported_calculations.each do |deletable| options.delete(deletable) end options.delete(:column_name) options.delete(:filter_on) options.delete(:cache_for) options end end end ActiveRecord::Base.send(:include, Statistics)