New upstream version 12.1.13

This commit is contained in:
Sruthi Chandran 2019-10-03 22:38:16 +05:30
parent 6dae2bc86e
commit 49063bcc27
10 changed files with 550 additions and 0 deletions

8
statistics/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*~
*.swp
*.swo
.DS_Store
debug.log
coverage
coverage.data
pkg/

View file

@ -0,0 +1 @@
Version 0.1, 27.05.09 - initial release

22
statistics/MIT-LICENSE Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) 2009 Alexandru Catighera
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

124
statistics/README.markdown Normal file
View file

@ -0,0 +1,124 @@
# Statistics
This ActiverRecord plugin allows you to easily define and pull statistics for AR models. This plugin was built with reporting in mind.
## Installation
gem install statistics
OR
script/plugin install git://github.com/acatighera/statistics.git
## Examples
#### Defining statistics is similar to defining named scopes. Strings and symbols both work as names.
class Account < ActiveRecord::Base
define_statistic :user_count, :count => :all
define_statistic :average_age, :average => :all, :column_name => 'age'
define_statistic 'subscriber count', :count => :all, :conditions => "subscription_opt_in = 1"
end
class Donations < ActiveRecord::Base
define_statistic :total_donations, :sum => :all, :column_name => "amount"
end
#### Actually pulling the numbers is simple:
#####for all stats
Account.statistics # returns { :user_count => 120, :average_age => 28, 'subscriber count' => 74 }
#####for a single stat
Account.get_stat(:user_count) # returns 120
### Here are some additional benefits of using this plugin:
#### Easily Filter
Note: I found filtering to be an important part of reporting (ie. filtering by date). All filters are optional so even if you define them you dont have to use them when pulling data. Using the `filter_all_stats_on` method and `:joins` options you can make things filterable by the same things which I found to be extremely useful.
class Account < ActiveRecord::Base
define_statistic :user_count, :count => :all, :filter_on => { :state => 'state = ?', :created_after => 'DATE(created_at) > ?'}
define_statistic :subscriber_count, :count => :all, :conditions => "subscription_opt_in = true"
filter_all_stats_on(:account_type, "account_type = ?")
end
Account.statistics(:account_type => 'non-admin')
Account.get_stat(:user_count, :account_type => 'non-admin', :created_after => 2009-01-01, :state => 'NY')
# NOTE: filters are optional (ie. no filters will be applied if none are passed in)
Account.get_stat(:user_count)
#### Caching
This is a new feature that uses `Rails.cache`. You can cache certain statistics for a specified amount of time (see below). By default caching is disabled if you do not pass in the `:cache_for` option. It is also important to note that caching is scoped by filters, there is no way around this since different filters produce different values.
class Account < ActiveRecord::Base
define_statistic :user_count, :count => :all, :cache_for => 30.minutes, :filter_on { :state => 'state = ?' }
end
Account.statistics(:state => 'NY') # This call generates a SQL query
Account.statistics(:state => 'NY') # This call and subsequent calls for the next 30 minutes will use the cached value
Account.statistics(:state => 'PA') # This call generates a SQL query because the user count for NY and PA could be different (and probably is)
Note: If you want Rails.cache to work properly, you need to use mem_cache_store in your rails enviroment file (ie. `config.cache_store = :mem_cache_store` in your enviroment.rb file).
#### Standardized
All ActiveRecord classes now respond to `statistics` and `get_stat` methods
all_stats = []
[ Account, Post, Comment ].each do |ar|
all_stats << ar.statistics
end
#### Calculated statistics (DRY)
You can define calculated metrics in order to perform mathematical calculations on one or more defined statistics.
class Account < ActiveRecord::Base
has_many :donations
define_statistic :user_count, :count => :all
define_statistic :total_donations, :sum => :all, :column_name => 'donations.amount', :joins => :donations
define_calculated_statistic :average_donation_per_user do
defined_stats(:total_donations) / defined_stats(:user_count)
end
filter_all_stats_on(:account_type, "account_type = ?")
filter_all_stats_on(:state, "state = ?")
filter_all_stats_on(:created_after, "DATE(created_at) > ?")
end
Pulling stats for calculated metrics is the same as for regular statistics. They also work with filters like regular statistics!
Account.get_stat(:average_donation_per_user, :account_type => 'non-admin', :state => 'NY')
Account.get_stat(:average_donation_per_user, :created_after => '2009-01-01')
#### Reuse scopes you already have defined
You can reuse the code you have written to do reporting.
class Account < ActiveRecord::Base
has_many :posts
named_scope :not_admins, :conditions => “account_type = non-admin
named_scope :accounts_with_posts, :joins => :posts
define_statistic :active_users_count, :count => [:not_admins, :accounts_with_posts]
end
#### Accepts all ActiveRecord::Calculations options
The `:conditions` and `:joins` options are all particularly useful
class Account < ActiveRecord::Base
has_many :posts
define_statistic :active_users_count, :count => :all, :joins => :posts, :conditions => "account_type = 'non-admin'"
end
###### Copyright (c) 2009 Alexandru Catighera, released under MIT license

13
statistics/Rakefile Normal file
View file

@ -0,0 +1,13 @@
begin
require 'jeweler'
Jeweler::Tasks.new do |gemspec|
gemspec.name = "statistics"
gemspec.summary = "An ActiveRecord gem that makes it easier to do reporting."
gemspec.email = "acatighera@gmail.com"
gemspec.homepage = "http://github.com/acatighera/statistics"
gemspec.authors = ["Alexandru Catighera"]
end
rescue LoadError
puts "Jeweler not available. Install it with: sudo gem install jeweler"
end

1
statistics/VERSION Normal file
View file

@ -0,0 +1 @@
1.0.0

1
statistics/init.rb Normal file
View file

@ -0,0 +1 @@
require File.join(File.dirname(__FILE__), 'lib', 'statistics')

View file

@ -0,0 +1,181 @@
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<<self; self; end).instance_eval do
define_method(method_name) do |filters|
# check the cache before running a query for the stat
cached_val = Rails.cache.read("#{self.name}#{method_name}#{filters}") if options[:cache_for]
return cached_val unless cached_val.nil?
scoped_options = Marshal.load(Marshal.dump(options))
filters.each do |key, value|
unless value.nil?
sql = ((@filter_all_on || {}).merge(scoped_options[:filter_on] || {}))[key].gsub("?", "'#{value}'")
sql = sql.gsub("%t", "#{table_name}")
sql_frag = send(:sanitize_sql_for_conditions, sql)
case
when sql_frag.nil? then nil
when scoped_options[:conditions].nil? then scoped_options[:conditions] = sql_frag
when scoped_options[:conditions].is_a?(Array) then scoped_options[:conditions][0].concat(" AND #{sql_frag}")
when scoped_options[:conditions].is_a?(String) then scoped_options[:conditions].concat(" AND #{sql_frag}")
end
end
end if filters.is_a?(Hash)
base = self
# chain named scopes
scopes = Array(scoped_options[calculation])
scopes.each do |scope|
base = base.send(scope)
end if scopes != [:all]
stat_value = base.send(calculation, scoped_options[:column_name], sql_options(scoped_options))
# cache stat value
Rails.cache.write("#{self.name}#{method_name}#{filters}", stat_value, :expires_in => 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<<self; self; end).instance_eval do
define_method(method_name) do |filters|
@filters = filters
yield
end
end
end
# returns an array containing the names/keys of all defined statistics
def statistics_keys
@statistics.keys
end
# Calculates all the statistics defined for this AR class and returns a hash with the values.
# There is an optional parameter that is a hash of all values you want to filter by.
#
# EXAMPLE:
# MockModel.statistics
# MockModel.statistics(:user_type => '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)

View file

@ -0,0 +1,48 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{statistics}
s.version = "1.0.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Alexandru Catighera"]
s.date = %q{2010-09-10}
s.email = %q{acatighera@gmail.com}
s.extra_rdoc_files = [
"README.markdown"
]
s.files = [
".gitignore",
"CHANGES.markdown",
"MIT-LICENSE",
"README.markdown",
"Rakefile",
"VERSION",
"init.rb",
"lib/statistics.rb",
"statistics.gemspec",
"test/statistics_test.rb"
]
s.homepage = %q{http://github.com/acatighera/statistics}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.5}
s.summary = %q{An ActiveRecord gem that makes it easier to do reporting.}
s.test_files = [
"test/statistics_test.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else
end
else
end
end

View file

@ -0,0 +1,151 @@
require 'test/unit'
require 'rubygems'
gem 'activerecord', '>= 1.15.4.7794'
gem 'mocha', '>= 0.9.0'
require 'active_record'
require 'active_support'
require 'mocha'
require "#{File.dirname(__FILE__)}/../init"
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
class Rails
def self.cache
ActiveSupport::Cache::MemCacheStore.new
end
end
class StatisticsTest < Test::Unit::TestCase
class BasicModel < ActiveRecord::Base
define_statistic :basic_num, :count => :all
end
class MockModel < ActiveRecord::Base
define_statistic "Basic Count", :count => :all
define_statistic :symbol_count, :count => :all
define_statistic "Basic Sum", :sum => :all, :column_name => 'amount'
define_statistic "Chained Scope Count", :count => [:all, :named_scope]
define_statistic "Default Filter", :count => :all
define_statistic "Custom Filter", :count => :all, :filter_on => { :channel => 'channel = ?', :start_date => 'DATE(created_at) > ?', :blah => 'blah = ?' }
define_statistic "Cached", :count => :all, :filter_on => { :channel => 'channel = ?', :blah => 'blah = ?' }, :cache_for => 1.second
define_calculated_statistic "Total Amount" do
defined_stats('Basic Sum') * defined_stats('Basic Count')
end
filter_all_stats_on(:user_id, "user_id = ?")
end
def test_basic
BasicModel.expects(:basic_num_stat).returns(1)
assert_equal({ :basic_num => 1 }, BasicModel.statistics)
end
def test_statistics
MockModel.expects(:basic_count_stat).returns(2)
MockModel.expects(:symbol_count_stat).returns(2)
MockModel.expects(:basic_sum_stat).returns(27)
MockModel.expects(:chained_scope_count_stat).returns(4)
MockModel.expects(:default_filter_stat).returns(5)
MockModel.expects(:custom_filter_stat).returns(3)
MockModel.expects(:cached_stat).returns(9)
MockModel.expects(:total_amount_stat).returns(54)
["Basic Count",
:symbol_count,
"Basic Sum",
"Chained Scope Count",
"Default Filter",
"Custom Filter",
"Cached",
"Total Amount"].each do |key|
assert MockModel.statistics_keys.include?(key)
end
assert_equal({ "Basic Count" => 2,
:symbol_count => 2,
"Basic Sum" => 27,
"Chained Scope Count" => 4,
"Default Filter" => 5,
"Custom Filter" => 3,
"Cached" => 9,
"Total Amount" => 54 }, MockModel.statistics)
end
def test_get_stat
MockModel.expects(:calculate).with(:count, :id, {}).returns(3)
assert_equal 3, MockModel.get_stat("Basic Count")
MockModel.expects(:calculate).with(:count, :id, { :conditions => "user_id = '54321'"}).returns(4)
assert_equal 4, MockModel.get_stat("Basic Count", :user_id => 54321)
end
def test_basic_stat
MockModel.expects(:calculate).with(:count, :id, {}).returns(3)
assert_equal 3, MockModel.basic_count_stat({})
MockModel.expects(:calculate).with(:sum, 'amount', {}).returns(31)
assert_equal 31, MockModel.basic_sum_stat({})
end
def test_chained_scope_stat
MockModel.expects(:all).returns(MockModel)
MockModel.expects(:named_scope).returns(MockModel)
MockModel.expects(:calculate).with(:count, :id, {}).returns(5)
assert_equal 5, MockModel.chained_scope_count_stat({})
end
def test_calculated_stat
MockModel.expects(:basic_count_stat).returns(3)
MockModel.expects(:basic_sum_stat).returns(33)
assert_equal 99, MockModel.total_amount_stat({})
MockModel.expects(:basic_count_stat).with(:user_id => 5).returns(2)
MockModel.expects(:basic_sum_stat).with(:user_id => 5).returns(25)
assert_equal 50, MockModel.total_amount_stat({:user_id => 5})
MockModel.expects(:basic_count_stat).with(:user_id => 6).returns(3)
MockModel.expects(:basic_sum_stat).with(:user_id => 6).returns(60)
assert_equal 180, MockModel.total_amount_stat({:user_id => 6})
end
def test_default_filter_stat
MockModel.expects(:calculate).with(:count, :id, {}).returns(8)
assert_equal 8, MockModel.default_filter_stat({})
MockModel.expects(:calculate).with(:count, :id, { :conditions => "user_id = '12345'" }).returns(2)
assert_equal 2, MockModel.default_filter_stat( :user_id => '12345' )
end
def test_custom_filter_stat
MockModel.expects(:calculate).with(:count, :id, {}).returns(6)
assert_equal 6, MockModel.custom_filter_stat({})
MockModel.expects(:calculate).with() do |param1, param2, param3|
param1 == :count &&
param2 == :id &&
(param3 == { :conditions => "channel = 'chan5' AND DATE(created_at) > '#{Date.today.to_s(:db)}'" } ||
param3 == { :conditions => "DATE(created_at) > '#{Date.today.to_s(:db)}' AND channel = 'chan5'" } )
end.returns(3)
assert_equal 3, MockModel.custom_filter_stat(:channel => 'chan5', :start_date => Date.today.to_s(:db))
end
def test_cached_stat
MockModel.expects(:calculate).returns(6)
assert_equal 6, MockModel.cached_stat({:channel => 'chan5'})
MockModel.stubs(:calculate).returns(8)
assert_equal 6, MockModel.cached_stat({:channel => 'chan5'})
assert_equal 8, MockModel.cached_stat({})
sleep(1)
assert_equal 8, MockModel.cached_stat({:channel => 'chan5'})
end
end