Merge branch 'master' of git://github.com/saulabs/reportable - reportable - Fork of reportable required by WarVox, from hdm/reportable.
(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
==========
+[](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'])