From 49063bcc27c46d608ae6f80485df7544b614f9b5 Mon Sep 17 00:00:00 2001 From: Sruthi Chandran Date: Thu, 3 Oct 2019 22:38:16 +0530 Subject: [PATCH] New upstream version 12.1.13 --- statistics/.gitignore | 8 ++ statistics/CHANGES.markdown | 1 + statistics/MIT-LICENSE | 22 ++++ statistics/README.markdown | 124 ++++++++++++++++++++ statistics/Rakefile | 13 +++ statistics/VERSION | 1 + statistics/init.rb | 1 + statistics/lib/statistics.rb | 181 +++++++++++++++++++++++++++++ statistics/statistics.gemspec | 48 ++++++++ statistics/test/statistics_test.rb | 151 ++++++++++++++++++++++++ 10 files changed, 550 insertions(+) create mode 100644 statistics/.gitignore create mode 100644 statistics/CHANGES.markdown create mode 100644 statistics/MIT-LICENSE create mode 100644 statistics/README.markdown create mode 100644 statistics/Rakefile create mode 100644 statistics/VERSION create mode 100644 statistics/init.rb create mode 100644 statistics/lib/statistics.rb create mode 100644 statistics/statistics.gemspec create mode 100644 statistics/test/statistics_test.rb diff --git a/statistics/.gitignore b/statistics/.gitignore new file mode 100644 index 0000000000..42cd07d74f --- /dev/null +++ b/statistics/.gitignore @@ -0,0 +1,8 @@ +*~ +*.swp +*.swo +.DS_Store +debug.log +coverage +coverage.data +pkg/ diff --git a/statistics/CHANGES.markdown b/statistics/CHANGES.markdown new file mode 100644 index 0000000000..f87780db89 --- /dev/null +++ b/statistics/CHANGES.markdown @@ -0,0 +1 @@ +Version 0.1, 27.05.09 - initial release \ No newline at end of file diff --git a/statistics/MIT-LICENSE b/statistics/MIT-LICENSE new file mode 100644 index 0000000000..8728f544e4 --- /dev/null +++ b/statistics/MIT-LICENSE @@ -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. diff --git a/statistics/README.markdown b/statistics/README.markdown new file mode 100644 index 0000000000..a50fdbbc32 --- /dev/null +++ b/statistics/README.markdown @@ -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 don’t 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 diff --git a/statistics/Rakefile b/statistics/Rakefile new file mode 100644 index 0000000000..dc7d54ce3d --- /dev/null +++ b/statistics/Rakefile @@ -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 + diff --git a/statistics/VERSION b/statistics/VERSION new file mode 100644 index 0000000000..3eefcb9dd5 --- /dev/null +++ b/statistics/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/statistics/init.rb b/statistics/init.rb new file mode 100644 index 0000000000..59818f0e07 --- /dev/null +++ b/statistics/init.rb @@ -0,0 +1 @@ +require File.join(File.dirname(__FILE__), 'lib', 'statistics') diff --git a/statistics/lib/statistics.rb b/statistics/lib/statistics.rb new file mode 100644 index 0000000000..020468657a --- /dev/null +++ b/statistics/lib/statistics.rb @@ -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< 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) diff --git a/statistics/statistics.gemspec b/statistics/statistics.gemspec new file mode 100644 index 0000000000..b83135ad2a --- /dev/null +++ b/statistics/statistics.gemspec @@ -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 + diff --git a/statistics/test/statistics_test.rb b/statistics/test/statistics_test.rb new file mode 100644 index 0000000000..d023a242aa --- /dev/null +++ b/statistics/test/statistics_test.rb @@ -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