Merge branch 'master' of git://github.com/saulabs/reportable - reportable - Unnamed repository; edit this file 'description' to name the repository. (DIR) Log (DIR) Files (DIR) Refs (DIR) README --- (DIR) commit e2a282013a4ad968a58806bb0fc8e3efbab5bd8d (DIR) parent f8532eeb704c511bb75b5bd4fd9656243c9b118b (HTM) Author: HD Moore <hd_moore@rapid7.com> Date: Mon, 8 Sep 2014 01:08:56 -0500 Merge branch 'master' of git://github.com/saulabs/reportable Conflicts: lib/saulabs/reportable/report.rb lib/saulabs/reportable/report_cache.rb spec/classes/report_spec.rb Diffstat: M .travis.yml | 2 ++ M Gemfile | 14 +++++--------- M README.md | 1 + M lib/saulabs/reportable.rb | 2 ++ M lib/saulabs/reportable/cumulated_r… | 2 +- M lib/saulabs/reportable/report.rb | 23 +++++++++++++---------- M lib/saulabs/reportable/report_cach… | 65 +++++++++++++------------------ M lib/saulabs/reportable/reporting_p… | 2 +- M reportable.gemspec | 2 +- M spec/classes/report_cache_spec.rb | 27 +++++++-------------------- M spec/classes/report_spec.rb | 9 +++++---- M spec/spec_helper.rb | 10 ++++++++-- 12 files changed, 74 insertions(+), 85 deletions(-) --- (DIR) diff --git a/.travis.yml b/.travis.yml @@ -1,4 +1,6 @@ language: ruby rvm: - 1.9.3 + - 2.0.0 + - 2.1.1 script: "rake spec" (DIR) diff --git a/Gemfile b/Gemfile @@ -1,16 +1,12 @@ source "http://rubygems.org" -gem 'rails', '~> 3.2.0' -gem 'activerecord', '~> 3.2.0', :require => 'active_record' -gem 'activesupport', '~> 3.2.0', :require => 'active_support' -gem 'actionpack', '~> 3.2.0', :require => 'action_pack' +gem 'rails', '~> 4.1.0' +gem 'protected_attributes' -gem 'sqlite3-ruby', '>= 1.2.0' -gem 'mysql', '>= 2.8.0' -gem 'pg', '>= 0.9.0' -gem 'tzinfo', '>= 0.3.0' +gem 'sqlite3' +# gem 'mysql', '>= 2.8.0' +gem 'pg' -gem 'rake', '>= 0.8.7' gem 'rspec', '~> 2.8.0' gem 'simplecov' gem 'excellent', '>= 1.5.4' (DIR) diff --git a/README.md b/README.md @@ -1,5 +1,6 @@ Reportable ========== +[![Build Status](https://travis-ci.org/saulabs/reportable.png?branch=master)](https://travis-ci.org/saulabs/reportable) Reportable allows for the easy creation of reports based on `ActiveRecord` models. (DIR) diff --git a/lib/saulabs/reportable.rb b/lib/saulabs/reportable.rb @@ -38,6 +38,8 @@ module Saulabs # the number of reporting periods to get (see +:grouping+) # @option options [Hash] :conditions ({}) # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported; + # @option options [Hash] :include ({}) + # include like in +ActiveRecord::Base#find+; names associations that should be loaded alongside; the symbols named refer to already defined associations # @option options [Boolean] :live_data (false) # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b> # @option options [DateTime, Boolean] :end_date (false) (DIR) diff --git a/lib/saulabs/reportable/cumulated_report.rb b/lib/saulabs/reportable/cumulated_report.rb @@ -34,7 +34,7 @@ module Saulabs def initial_cumulative_value(date, options) conditions = setup_conditions(nil, date, options[:conditions]) - @klass.send(@aggregation, @value_column, :conditions => conditions) + @klass.where(conditions).calculate(@aggregation, @value_column) end end (DIR) diff --git a/lib/saulabs/reportable/report.rb b/lib/saulabs/reportable/report.rb @@ -54,6 +54,8 @@ module Saulabs # the number of reporting periods to get (see +:grouping+) # @option options [Hash] :conditions ({}) # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported; + # @option options [Hash] :include ({}) + # include like in +ActiveRecord::Base#find+; names associations that should be loaded alongside; the symbols named refer to already defined associations # @option options [Boolean] :live_data (false) # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b> # @option options [DateTime, Boolean] :end_date (false) @@ -71,6 +73,7 @@ module Saulabs @options = { :limit => options[:limit] || 100, :distinct => options[:distinct] || false, + :include => options[:include] || [], :conditions => options[:conditions] || [], :grouping => Grouping.new(options[:grouping] || :day), :live_data => options[:live_data] || false, @@ -122,14 +125,14 @@ module Saulabs def read_data(begin_at, end_at, options) conditions = setup_conditions(begin_at, end_at, options[:conditions]) - @klass.send(@aggregation, - @value_column, - :conditions => conditions, - :distinct => options[:distinct], - :group => options[:grouping].to_sql(@date_column), - :order => "#{options[:grouping].to_sql(@date_column)} ASC", - :limit => options[:limit] - ) + table_name = ActiveRecord::Base.connection.quote_table_name(@klass.table_name) + date_column = ActiveRecord::Base.connection.quote_column_name(@date_column.to_s) + grouping = options[:grouping].to_sql("#{table_name}.#{date_column}") + order = "#{grouping} ASC" + + @klass.where(conditions).includes(options[:include]).distinct(options[:distinct]). + group(grouping).order(order).limit(options[:limit]). + calculate(@aggregation, @value_column) end def setup_conditions(begin_at, end_at, custom_conditions = []) @@ -153,13 +156,13 @@ module Saulabs case context when :initialize options.each_key do |k| - raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :distinct, :date_column, :value_column, :conditions, :live_data, :end_date, :cacheable].include?(k) + raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :distinct, :include, :date_column, :value_column, :conditions, :live_data, :end_date].include?(k) end raise ArgumentError.new("Invalid aggregation #{options[:aggregation]}!") if options[:aggregation] && ![:count, :sum, :maximum, :minimum, :average].include?(options[:aggregation]) raise ArgumentError.new('The name of the column holding the value to sum has to be specified for aggregation :sum!') if [:sum, :maximum, :minimum, :average].include?(options[:aggregation]) && !options.key?(:value_column) when :run options.each_key do |k| - raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :grouping, :live_data, :end_date].include?(k) + raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :include, :grouping, :live_data, :end_date].include?(k) end end raise ArgumentError.new('Options :live_data and :end_date may not both be specified!') if options[:live_data] && options[:end_date] (DIR) diff --git a/lib/saulabs/reportable/report_cache.rb b/lib/saulabs/reportable/report_cache.rb @@ -20,7 +20,7 @@ module Saulabs validates_presence_of :value validates_presence_of :reporting_period - attr_accessible :model_name, :report_name, :grouping, :aggregation, :value, :reporting_period, :conditions + # attr_accessible :model_name, :report_name, :grouping, :aggregation, :value, :reporting_period, :conditions self.skip_time_zone_conversion_for_attributes = [:reporting_period] @@ -40,10 +40,7 @@ module Saulabs # Saulabs::Reportable::ReportCache.clear_for(User, :registrations) # def self.clear_for(klass, report) - self.delete_all(:conditions => { - :model_name => klass.name, - :report_name => report.to_s - }) + self.where(model_name: klass.name, report_name: report.to_s).delete_all end # Processes the report using the respective cache. @@ -90,21 +87,20 @@ module Saulabs private def self.prepare_result(new_data, cached_data, report, options) - new_data = new_data.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] } - cached_data.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] } + new_data = new_data.to_a.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] } + cached_data.to_a.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] } current_reporting_period = ReportingPeriod.new(options[:grouping]) reporting_period = get_first_reporting_period(options) result = [] while reporting_period < (options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).next : current_reporting_period) if options[:cacheable] and cached = cached_data.find { |cached| reporting_period == cached[0] } result << [cached[0].date_time, cached[1]] + elsif reporting_period.last_date_time.past? + new_cached = build_cached_data(report, options[:grouping], options[:conditions], reporting_period, find_value(new_data, reporting_period)) + new_cached.save! + result << [reporting_period.date_time, new_cached.value] else - value = find_value(new_data, reporting_period) - if options[:cacheable] - new_cached = build_cached_data(report, options[:grouping], options[:conditions], reporting_period, value) - new_cached.save! if options[:cacheable] - end - result << [reporting_period.date_time, value] + result << [reporting_period.date_time, find_value(new_data, reporting_period)] end reporting_period = reporting_period.next end @@ -142,36 +138,31 @@ module Saulabs end def self.read_cached_data(report, options) - return [] if not options[:cacheable] - options[:conditions] ||= [] - conditions = [ - %w(model_name report_name grouping aggregation conditions).map do |column_name| - "#{self.connection.quote_column_name(column_name)} = ?" - end.join(' AND '), - report.klass.to_s, - report.name.to_s, - options[:grouping].identifier.to_s, - report.aggregation.to_s, - serialize_conditions(options[:conditions]) - ] - first_reporting_period = get_first_reporting_period(options) + conditions = build_conditions_for_reading_cached_data(report, options) + conditions.limit(options[:limit]).order('reporting_period ASC') + end + + def self.build_conditions_for_reading_cached_data(report, options) + start_date = get_first_reporting_period(options).date_time + + conditions = where('reporting_period >= ?', start_date).where( + model_name: report.klass.to_s, + report_name: report.name.to_s, + grouping: options[:grouping].identifier.to_s, + aggregation: report.aggregation.to_s, + conditions: serialize_conditions(options[:conditions] || []) + ) + if options[:end_date] - conditions.first << ' AND reporting_period BETWEEN ? AND ?' - conditions << first_reporting_period.date_time - conditions << ReportingPeriod.new(options[:grouping], options[:end_date]).date_time + end_date = ReportingPeriod.new(options[:grouping], options[:end_date]).date_time + conditions.where('reporting_period <= ?', end_date) else - conditions.first << ' AND reporting_period >= ?' - conditions << first_reporting_period.date_time + conditions end - self.all( - :conditions => conditions, - :limit => options[:limit], - :order => 'reporting_period ASC' - ) end def self.read_new_data(cached_data, options, &block) - return [] if !options[:live_data] && cached_data.length == options[:limit] + return [] if !options[:live_data] && cached_data.size == options[:limit] first_reporting_period_to_read = get_first_reporting_period_to_read(cached_data, options) last_reporting_period_to_read = options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).last_date_time : nil (DIR) diff --git a/lib/saulabs/reportable/reporting_period.rb b/lib/saulabs/reportable/reporting_period.rb @@ -70,7 +70,7 @@ module Saulabs # the reporting period for the {Saulabs::Reportable::Grouping} as parsed from the db string # def self.from_db_string(grouping, db_string) - return self.new(grouping, db_string) if db_string.is_a?(Date) + return self.new(grouping, db_string) if db_string.is_a?(Date) || db_string.is_a?(Time) parts = grouping.date_parts_from_db_string(db_string.to_s) case grouping.identifier when :hour (DIR) diff --git a/reportable.gemspec b/reportable.gemspec @@ -14,7 +14,7 @@ pkg_files += Dir['spec/**/*.{rb,yml,opts}'] Gem::Specification.new do |s| s.name = %q{reportable} - s.version = '1.2.0' + s.version = '1.3.1' s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=) s.authors = ['Marco Otte-Witte', 'Martin Kavalar'] (DIR) diff --git a/spec/classes/report_cache_spec.rb b/spec/classes/report_cache_spec.rb @@ -91,19 +91,6 @@ describe Saulabs::Reportable::ReportCache do end - describe '.clear_for' do - - it 'should delete all entries in the cache for the klass and report name' do - Saulabs::Reportable::ReportCache.should_receive(:delete_all).once.with(:conditions => { - :model_name => User.name, - :report_name => 'registrations' - }) - - Saulabs::Reportable::ReportCache.clear_for(User, :registrations) - end - - end - describe '.process' do before do @@ -136,7 +123,7 @@ describe Saulabs::Reportable::ReportCache do end it 'should yield the first reporting period if not all required data could be retrieved from the cache' do - Saulabs::Reportable::ReportCache.stub!(:all).and_return([Saulabs::Reportable::ReportCache.new]) + Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return([Saulabs::Reportable::ReportCache.new]) Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at| begin_at.should == Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], @report.options[:limit]).date_time @@ -152,7 +139,7 @@ describe Saulabs::Reportable::ReportCache do ) cached = Saulabs::Reportable::ReportCache.new cached.stub!(:reporting_period).and_return(reporting_period.date_time) - Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit] - 1, Saulabs::Reportable::ReportCache.new), cached) + Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return(Array.new(@report.options[:limit] - 1, Saulabs::Reportable::ReportCache.new), cached) Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at| begin_at.should == reporting_period.date_time @@ -166,7 +153,7 @@ describe Saulabs::Reportable::ReportCache do describe 'with :live_data = false' do it 'should not yield if all required data could be retrieved from the cache' do - Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit], Saulabs::Reportable::ReportCache.new)) + Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return(Array.new(@report.options[:limit], Saulabs::Reportable::ReportCache.new)) lambda { Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new } @@ -174,7 +161,7 @@ describe Saulabs::Reportable::ReportCache do end it 'should yield to the block if no data could be retrieved from the cache' do - Saulabs::Reportable::ReportCache.stub!(:all).and_return([]) + Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return([]) lambda { Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new } @@ -200,7 +187,7 @@ describe Saulabs::Reportable::ReportCache do end - it 'should read existing data from the cache' do + xit 'should read existing data from the cache' do Saulabs::Reportable::ReportCache.should_receive(:all).once.with( :conditions => [ %w(model_name report_name grouping aggregation conditions).map do |column_name| @@ -220,7 +207,7 @@ describe Saulabs::Reportable::ReportCache do Saulabs::Reportable::ReportCache.process(@report, @report.options) { [] } end - it 'should utilize the end_date in the conditions' do + xit 'should utilize the end_date in the conditions' do end_date = Time.now - 1.send(@report.options[:grouping].identifier) Saulabs::Reportable::ReportCache.should_receive(:all).once.with( :conditions => [ @@ -242,7 +229,7 @@ describe Saulabs::Reportable::ReportCache do Saulabs::Reportable::ReportCache.process(@report, @report.options.merge(:end_date => end_date)) { [] } end - it "should read existing data from the cache for the correct grouping if one other than the report's default grouping is specified" do + xit "should read existing data from the cache for the correct grouping if one other than the report's default grouping is specified" do grouping = Saulabs::Reportable::Grouping.new(:month) Saulabs::Reportable::ReportCache.should_receive(:all).once.with( :conditions => [ (DIR) diff --git a/spec/classes/report_spec.rb b/spec/classes/report_spec.rb @@ -21,7 +21,8 @@ describe Saulabs::Reportable::Report do it 'should process the data with the report cache' do Saulabs::Reportable::ReportCache.should_receive(:process).once.with( @report, - { :limit => 100, :grouping => @report.options[:grouping], :conditions => [], :live_data => false, :end_date => false, :distinct => false, :cacheable => true } + + { :limit => 100, :grouping => @report.options[:grouping], :conditions => [], :include => [], :live_data => false, :end_date => false, :distinct => false } ) @report.run @@ -30,7 +31,7 @@ describe Saulabs::Reportable::Report do it 'should process the data with the report cache when custom conditions are given' do Saulabs::Reportable::ReportCache.should_receive(:process).once.with( @report, - { :limit => 100, :grouping => @report.options[:grouping], :conditions => { :some => :condition }, :live_data => false, :end_date => false, :distinct => false, :cacheable => true } + { :limit => 100, :grouping => @report.options[:grouping], :conditions => { :some => :condition }, :include => [], :live_data => false, :end_date => false, :distinct => false } ) @report.run(:conditions => { :some => :condition }) @@ -47,7 +48,7 @@ describe Saulabs::Reportable::Report do Saulabs::Reportable::Grouping.should_receive(:new).once.with(:month).and_return(grouping) Saulabs::Reportable::ReportCache.should_receive(:process).once.with( @report, - { :limit => 100, :grouping => grouping, :conditions => [], :live_data => false, :end_date => false, :distinct => false, :cacheable => true } + { :limit => 100, :grouping => grouping, :conditions => [], :live_data => false, :end_date => false, :distinct => false, :include => [] } ) @report.run(:grouping => :month) @@ -581,7 +582,7 @@ describe Saulabs::Reportable::Report do describe '#read_data' do - it 'should invoke the aggregation method on the model' do + xit 'should invoke the aggregation method on the model' do @report = Saulabs::Reportable::Report.new(User, :registrations, :aggregation => :count) User.should_receive(:count).once.and_return([]) (DIR) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb @@ -27,8 +27,14 @@ require File.join(ROOT, 'lib', 'saulabs', 'reportable.rb') # config.time_zone = 'Pacific Time (US & Canada)' # end -FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log') -ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/log/spec.log") +# FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log') +# ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/log/spec.log") + +RSpec.configure do |config| + config.filter_run :focus => true + config.run_all_when_everything_filtered = true +end +ActiveRecord::Base.default_timezone = :local databases = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml'))) ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'sqlite3'])