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'])