reportable 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c29f948fbe08b5fcd5fcb8c93b8eb080a5a63e39
4
+ data.tar.gz: e74b8522ddd8b95af32653a27b30aa056538ee68
5
+ SHA512:
6
+ metadata.gz: 991545e56bce51a1c3fdff4bc85487f63368d63b5b12d87598168c4955093d97cce4ef380e96609afe3e1f46f0ce98bccbeaa20991378100ea9484127b395963
7
+ data.tar.gz: 7a0e85284ec828a4e2eb77314d02e1304e584821f7aad8b172ffc06c0c590e26d79da66ed8b2922c6635b6936f1762c148d58d1a94001d930178c86460386725
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  Reportable
2
2
  ==========
3
+ [![Build Status](https://travis-ci.org/saulabs/reportable.png?branch=master)](https://travis-ci.org/saulabs/reportable)
3
4
 
4
5
  Reportable allows for the easy creation of reports based on `ActiveRecord` models.
5
6
 
data/Rakefile CHANGED
@@ -4,15 +4,14 @@ require 'bundler'
4
4
  Bundler.setup
5
5
  Bundler.require
6
6
 
7
- require 'spec/rake/spectask'
8
- require 'simplabs/excellent/rake'
7
+ require "rspec/core/rake_task"
8
+
9
9
 
10
10
  desc 'Default: run specs.'
11
11
  task :default => :spec
12
12
 
13
13
  desc 'Run the specs'
14
- Spec::Rake::SpecTask.new(:spec) do |t|
15
- t.spec_files = FileList['spec/**/*_spec.rb']
14
+ RSpec::Core::RakeTask.new(:spec) do |spec|
16
15
  end
17
16
 
18
17
  YARD::Rake::YardocTask.new(:doc) do |t|
@@ -20,6 +19,3 @@ YARD::Rake::YardocTask.new(:doc) do |t|
20
19
  t.options = ['--no-private', '--title', 'Reportable Documentation']
21
20
  end
22
21
 
23
- Simplabs::Excellent::Rake::ExcellentTask.new(:excellent) do |t|
24
- t.paths = %w(lib)
25
- end
@@ -2,6 +2,8 @@ class ReportableJqueryFlotAssetsGenerator < Rails::Generators::Base
2
2
 
3
3
  include Rails::Generators::Actions
4
4
 
5
+ source_root File.expand_path('../templates/', __FILE__)
6
+
5
7
  def create_jquery_flot_file
6
8
  empty_directory('public/javascripts')
7
9
  copy_file(
@@ -2,11 +2,10 @@ class ReportableMigrationGenerator < Rails::Generators::Base
2
2
 
3
3
  include Rails::Generators::Migration
4
4
 
5
+ source_root File.expand_path('../templates/', __FILE__)
6
+
5
7
  def create_migration
6
- migration_template(
7
- File.join(File.dirname(__FILE__), 'templates', 'migration.rb'),
8
- 'db/migrate/create_reportable_cache.rb'
9
- )
8
+ migration_template('migration.rb', 'db/migrate/create_reportable_cache.rb')
10
9
  end
11
10
 
12
11
  def self.next_migration_number(dirname)
@@ -2,6 +2,8 @@ class ReportableRaphaelAssetsGenerator < Rails::Generators::Base
2
2
 
3
3
  include Rails::Generators::Actions
4
4
 
5
+ source_root File.expand_path('../templates/', __FILE__)
6
+
5
7
  def create_raphael_file
6
8
  empty_directory('public/javascripts')
7
9
  copy_file(
@@ -38,6 +38,8 @@ module Saulabs
38
38
  # the number of reporting periods to get (see +:grouping+)
39
39
  # @option options [Hash] :conditions ({})
40
40
  # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
41
+ # @option options [Hash] :include ({})
42
+ # include like in +ActiveRecord::Base#find+; names associations that should be loaded alongside; the symbols named refer to already defined associations
41
43
  # @option options [Boolean] :live_data (false)
42
44
  # 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>
43
45
  # @option options [DateTime, Boolean] :end_date (false)
@@ -34,7 +34,7 @@ module Saulabs
34
34
 
35
35
  def initial_cumulative_value(date, options)
36
36
  conditions = setup_conditions(nil, date, options[:conditions])
37
- @klass.send(@aggregation, @value_column, :conditions => conditions)
37
+ @klass.where(conditions).calculate(@aggregation, @value_column)
38
38
  end
39
39
 
40
40
  end
@@ -41,6 +41,8 @@ module Saulabs
41
41
  from_sqlite_db_string(db_string)
42
42
  when /postgres/i
43
43
  from_postgresql_db_string(db_string)
44
+ when /mssql/i, /sqlserver/i
45
+ from_sqlserver_db_string(db_string)
44
46
  end
45
47
  end
46
48
 
@@ -57,6 +59,8 @@ module Saulabs
57
59
  sqlite_format(date_column)
58
60
  when /postgres/i
59
61
  postgresql_format(date_column)
62
+ when /mssql/i, /sqlserver/i
63
+ sqlserver_format(date_column)
60
64
  end
61
65
  end
62
66
 
@@ -94,6 +98,14 @@ module Saulabs
94
98
  end
95
99
  end
96
100
 
101
+ def from_sqlserver_db_string(db_string)
102
+ if @identifier == :week
103
+ parts = [db_string[0..3], db_string[5..6]].map(&:to_i)
104
+ else
105
+ db_string.split(/[- ]/).map(&:to_i)
106
+ end
107
+ end
108
+
97
109
  def mysql_format(date_column)
98
110
  case @identifier
99
111
  when :hour
@@ -133,6 +145,19 @@ module Saulabs
133
145
  end
134
146
  end
135
147
 
148
+ def sqlserver_format(date_column)
149
+ case @identifier
150
+ when :hour
151
+ "DATEADD(hh,DATEDIFF(hh,DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',#{date_column}), '1 Jan 1900'),#{date_column}), DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',#{date_column}), '1 Jan 1900'))"
152
+ when :day
153
+ "DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',#{date_column}), '1 Jan 1900')"
154
+ when :week
155
+ "LEFT(CONVERT(varchar,#{date_column},120), 4) + '-' + CAST(DATEPART(isowk,#{date_column}) AS VARCHAR)"
156
+ when :month
157
+ "DATEADD(mm,DATEDIFF(mm,'1 Jan 1900',#{date_column}), '1 Jan 1900')"
158
+ end
159
+ end
160
+
136
161
  end
137
162
 
138
163
  end
@@ -54,6 +54,8 @@ module Saulabs
54
54
  # the number of reporting periods to get (see +:grouping+)
55
55
  # @option options [Hash] :conditions ({})
56
56
  # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
57
+ # @option options [Hash] :include ({})
58
+ # include like in +ActiveRecord::Base#find+; names associations that should be loaded alongside; the symbols named refer to already defined associations
57
59
  # @option options [Boolean] :live_data (false)
58
60
  # 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>
59
61
  # @option options [DateTime, Boolean] :end_date (false)
@@ -68,6 +70,8 @@ module Saulabs
68
70
  @value_column = (options[:value_column] || (@aggregation == :count ? 'id' : name)).to_s
69
71
  @options = {
70
72
  :limit => options[:limit] || 100,
73
+ :distinct => options[:distinct] || false,
74
+ :include => options[:include] || [],
71
75
  :conditions => options[:conditions] || [],
72
76
  :grouping => Grouping.new(options[:grouping] || :day),
73
77
  :live_data => options[:live_data] || false,
@@ -115,13 +119,14 @@ module Saulabs
115
119
 
116
120
  def read_data(begin_at, end_at, options)
117
121
  conditions = setup_conditions(begin_at, end_at, options[:conditions])
118
- @klass.send(@aggregation,
119
- @value_column,
120
- :conditions => conditions,
121
- :group => options[:grouping].to_sql(@date_column),
122
- :order => "#{options[:grouping].to_sql(@date_column)} ASC",
123
- :limit => options[:limit]
124
- )
122
+ table_name = ActiveRecord::Base.connection.quote_table_name(@klass.table_name)
123
+ date_column = ActiveRecord::Base.connection.quote_column_name(@date_column.to_s)
124
+ grouping = options[:grouping].to_sql("#{table_name}.#{date_column}")
125
+ order = "#{grouping} ASC"
126
+
127
+ @klass.where(conditions).includes(options[:include]).distinct(options[:distinct]).
128
+ group(grouping).order(order).limit(options[:limit]).
129
+ calculate(@aggregation, @value_column)
125
130
  end
126
131
 
127
132
  def setup_conditions(begin_at, end_at, custom_conditions = [])
@@ -145,13 +150,13 @@ module Saulabs
145
150
  case context
146
151
  when :initialize
147
152
  options.each_key do |k|
148
- raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :date_column, :value_column, :conditions, :live_data, :end_date].include?(k)
153
+ raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :distinct, :include, :date_column, :value_column, :conditions, :live_data, :end_date].include?(k)
149
154
  end
150
155
  raise ArgumentError.new("Invalid aggregation #{options[:aggregation]}!") if options[:aggregation] && ![:count, :sum, :maximum, :minimum, :average].include?(options[:aggregation])
151
156
  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)
152
157
  when :run
153
158
  options.each_key do |k|
154
- raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :grouping, :live_data, :end_date].include?(k)
159
+ raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :include, :grouping, :live_data, :end_date].include?(k)
155
160
  end
156
161
  end
157
162
  raise ArgumentError.new('Options :live_data and :end_date may not both be specified!') if options[:live_data] && options[:end_date]
@@ -20,7 +20,7 @@ module Saulabs
20
20
  validates_presence_of :value
21
21
  validates_presence_of :reporting_period
22
22
 
23
- attr_accessible :model_name, :report_name, :grouping, :aggregation, :value, :reporting_period, :conditions
23
+ # attr_accessible :model_name, :report_name, :grouping, :aggregation, :value, :reporting_period, :conditions
24
24
 
25
25
  self.skip_time_zone_conversion_for_attributes = [:reporting_period]
26
26
 
@@ -40,10 +40,7 @@ module Saulabs
40
40
  # Saulabs::Reportable::ReportCache.clear_for(User, :registrations)
41
41
  #
42
42
  def self.clear_for(klass, report)
43
- self.delete_all(:conditions => {
44
- :model_name => klass.name,
45
- :report_name => report.to_s
46
- })
43
+ self.where(model_name: klass.name, report_name: report.to_s).delete_all
47
44
  end
48
45
 
49
46
  # Processes the report using the respective cache.
@@ -69,6 +66,15 @@ module Saulabs
69
66
  #
70
67
  def self.process(report, options, &block)
71
68
  raise ArgumentError.new('A block must be given') unless block_given?
69
+
70
+ # If end_date is in the middle of the current reporting period it means it requests live_data.
71
+ # Update the options hash to reflect reality.
72
+ current_reporting_period = ReportingPeriod.new(options[:grouping])
73
+ if options[:end_date] && options[:end_date] > current_reporting_period.date_time
74
+ options[:live_data] = true
75
+ options.delete(:end_date)
76
+ end
77
+
72
78
  self.transaction do
73
79
  cached_data = read_cached_data(report, options)
74
80
  new_data = read_new_data(cached_data, options, &block)
@@ -79,8 +85,8 @@ module Saulabs
79
85
  private
80
86
 
81
87
  def self.prepare_result(new_data, cached_data, report, options)
82
- new_data = new_data.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] }
83
- cached_data.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] }
88
+ new_data = new_data.to_a.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] }
89
+ cached_data.to_a.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] }
84
90
  current_reporting_period = ReportingPeriod.new(options[:grouping])
85
91
  reporting_period = get_first_reporting_period(options)
86
92
  result = []
@@ -128,45 +134,54 @@ module Saulabs
128
134
  end
129
135
 
130
136
  def self.read_cached_data(report, options)
131
- options[:conditions] ||= []
132
- conditions = [
133
- %w(model_name report_name grouping aggregation conditions).map do |column_name|
134
- "#{self.connection.quote_column_name(column_name)} = ?"
135
- end.join(' AND '),
136
- report.klass.to_s,
137
- report.name.to_s,
138
- options[:grouping].identifier.to_s,
139
- report.aggregation.to_s,
140
- serialize_conditions(options[:conditions])
141
- ]
142
- first_reporting_period = get_first_reporting_period(options)
143
- last_reporting_period = get_last_reporting_period(options)
144
- if last_reporting_period
145
- conditions.first << ' AND reporting_period BETWEEN ? AND ?'
146
- conditions << first_reporting_period.date_time
147
- conditions << last_reporting_period.date_time
137
+ conditions = build_conditions_for_reading_cached_data(report, options)
138
+ conditions.limit(options[:limit]).order('reporting_period ASC')
139
+ end
140
+
141
+ def self.build_conditions_for_reading_cached_data(report, options)
142
+ start_date = get_first_reporting_period(options).date_time
143
+
144
+ conditions = where('reporting_period >= ?', start_date).where(
145
+ model_name: report.klass.to_s,
146
+ report_name: report.name.to_s,
147
+ grouping: options[:grouping].identifier.to_s,
148
+ aggregation: report.aggregation.to_s,
149
+ conditions: serialize_conditions(options[:conditions] || [])
150
+ )
151
+
152
+ if options[:end_date]
153
+ end_date = ReportingPeriod.new(options[:grouping], options[:end_date]).date_time
154
+ conditions.where('reporting_period <= ?', end_date)
148
155
  else
149
- conditions.first << ' AND reporting_period >= ?'
150
- conditions << first_reporting_period.date_time
156
+ conditions
151
157
  end
152
- self.all(
153
- :conditions => conditions,
154
- :limit => options[:limit],
155
- :order => 'reporting_period ASC'
156
- )
157
158
  end
158
159
 
159
160
  def self.read_new_data(cached_data, options, &block)
160
- if !options[:live_data] && cached_data.length == options[:limit]
161
- []
161
+ return [] if !options[:live_data] && cached_data.size == options[:limit]
162
+
163
+ first_reporting_period_to_read = get_first_reporting_period_to_read(cached_data, options)
164
+ last_reporting_period_to_read = options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).last_date_time : nil
165
+
166
+ yield(first_reporting_period_to_read.date_time, last_reporting_period_to_read)
167
+ end
168
+
169
+ def self.get_first_reporting_period_to_read(cached_data, options)
170
+ return get_first_reporting_period(options) if cached_data.empty?
171
+
172
+ last_cached_reporting_period = ReportingPeriod.new(options[:grouping], cached_data.last.reporting_period)
173
+ missing_reporting_periods = options[:limit] - cached_data.length
174
+ last_reporting_period = if !options[:live_data] && options[:end_date]
175
+ ReportingPeriod.new(options[:grouping], options[:end_date])
162
176
  else
163
- first_reporting_period_to_read = if cached_data.length < options[:limit]
164
- get_first_reporting_period(options)
165
- else
166
- ReportingPeriod.new(options[:grouping], cached_data.last.reporting_period).next
167
- end
168
- last_reporting_period_to_read = options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).last_date_time : nil
169
- yield(first_reporting_period_to_read.date_time, last_reporting_period_to_read)
177
+ ReportingPeriod.new(options[:grouping]).previous
178
+ end
179
+
180
+ if missing_reporting_periods == 0 || last_cached_reporting_period.offset(missing_reporting_periods) == last_reporting_period
181
+ # cache only has missing data contiguously at the end
182
+ last_cached_reporting_period.next
183
+ else
184
+ get_first_reporting_period(options)
170
185
  end
171
186
  end
172
187
 
@@ -178,10 +193,6 @@ module Saulabs
178
193
  end
179
194
  end
180
195
 
181
- def self.get_last_reporting_period(options)
182
- return ReportingPeriod.new(options[:grouping], options[:end_date]) if options[:end_date]
183
- end
184
-
185
196
  end
186
197
 
187
198
  end
@@ -70,7 +70,7 @@ module Saulabs
70
70
  # the reporting period for the {Saulabs::Reportable::Grouping} as parsed from the db string
71
71
  #
72
72
  def self.from_db_string(grouping, db_string)
73
- return self.new(grouping, db_string) if db_string.is_a?(Date)
73
+ return self.new(grouping, db_string) if db_string.is_a?(Date) || db_string.is_a?(Time)
74
74
  parts = grouping.date_parts_from_db_string(db_string.to_s)
75
75
  case grouping.identifier
76
76
  when :hour
@@ -76,6 +76,30 @@ describe Saulabs::Reportable::Grouping do
76
76
 
77
77
  end
78
78
 
79
+ describe 'for MS SQL Server' do
80
+
81
+ before do
82
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('sqlserver')
83
+ end
84
+
85
+ it 'should output a format of "YYYY-MM-DD HH:00:00.0" for grouping :hour' do # string "%Y-%m-%d %h:00:00.0"
86
+ Saulabs::Reportable::Grouping.new(:hour).send(:to_sql, 'created_at').should == "DATEADD(hh,DATEDIFF(hh,DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',created_at), '1 Jan 1900'),created_at), DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',created_at), '1 Jan 1900'))"
87
+ end
88
+
89
+ it 'should output a format of "YYYY-MM-DD" for grouping :day' do
90
+ Saulabs::Reportable::Grouping.new(:day).send(:to_sql, 'created_at').should == "DATEADD(dd,DATEDIFF(dd,'1 Jan 1900',created_at), '1 Jan 1900')"
91
+ end
92
+
93
+ it 'should output a format of "YYYY-WW" for grouping :week' do
94
+ Saulabs::Reportable::Grouping.new(:week).send(:to_sql, 'created_at').should == "LEFT(CONVERT(varchar,created_at,120), 4) + '-' + CAST(DATEPART(isowk,created_at) AS VARCHAR)"
95
+ end
96
+
97
+ it 'should output a format of "YYYY-MM-01" for grouping :month' do
98
+ Saulabs::Reportable::Grouping.new(:month).send(:to_sql, 'created_at').should == "DATEADD(mm,DATEDIFF(mm,'1 Jan 1900',created_at), '1 Jan 1900')"
99
+ end
100
+
101
+ end
102
+
79
103
  end
80
104
 
81
105
  describe '#date_parts_from_db_string' do
@@ -94,7 +118,7 @@ describe Saulabs::Reportable::Grouping do
94
118
 
95
119
  end
96
120
 
97
- it 'should split the string with "-" and return teh calendar year and week for grouping :week' do
121
+ it 'should split the string with "-" and return the calendar year and week for grouping :week' do
98
122
  db_string = '2008-2-1'
99
123
  expected = [2008, 5]
100
124
 
@@ -150,6 +174,29 @@ describe Saulabs::Reportable::Grouping do
150
174
 
151
175
  end
152
176
 
177
+ describe 'for MS SQL Server' do
178
+
179
+ before do
180
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('sqlserver')
181
+ end
182
+
183
+ for grouping in [[:hour, '2008-12-31 12'], [:day, '2008-12-31'], [:month, '2008-12']] do
184
+
185
+ it "should split the string with '-' and ' ' for grouping :#{grouping[0].to_s}" do
186
+ Saulabs::Reportable::Grouping.new(grouping[0]).date_parts_from_db_string(grouping[1]).should == grouping[1].split(/[- ]/).map(&:to_i)
187
+ end
188
+
189
+ end
190
+
191
+ it 'should use the first 4 numbers for the year and the last 2 numbers for the week for grouping :week' do
192
+ db_string = '2008-52'
193
+ expected = [2008, 52]
194
+
195
+ Saulabs::Reportable::Grouping.new(:week).date_parts_from_db_string(db_string).should == expected
196
+ end
197
+
198
+ end
199
+
153
200
  end
154
201
 
155
202
  end
@@ -91,19 +91,6 @@ describe Saulabs::Reportable::ReportCache do
91
91
 
92
92
  end
93
93
 
94
- describe '.clear_for' do
95
-
96
- it 'should delete all entries in the cache for the klass and report name' do
97
- Saulabs::Reportable::ReportCache.should_receive(:delete_all).once.with(:conditions => {
98
- :model_name => User.name,
99
- :report_name => 'registrations'
100
- })
101
-
102
- Saulabs::Reportable::ReportCache.clear_for(User, :registrations)
103
- end
104
-
105
- end
106
-
107
94
  describe '.process' do
108
95
 
109
96
  before do
@@ -136,11 +123,7 @@ describe Saulabs::Reportable::ReportCache do
136
123
  end
137
124
 
138
125
  it 'should yield the first reporting period if not all required data could be retrieved from the cache' do
139
- reporting_period = Saulabs::Reportable::ReportingPeriod.new(
140
- @report.options[:grouping],
141
- Time.now - 3.send(@report.options[:grouping].identifier)
142
- )
143
- Saulabs::Reportable::ReportCache.stub!(:all).and_return([Saulabs::Reportable::ReportCache.new])
126
+ Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return([Saulabs::Reportable::ReportCache.new])
144
127
 
145
128
  Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at|
146
129
  begin_at.should == Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], @report.options[:limit]).date_time
@@ -156,7 +139,7 @@ describe Saulabs::Reportable::ReportCache do
156
139
  )
157
140
  cached = Saulabs::Reportable::ReportCache.new
158
141
  cached.stub!(:reporting_period).and_return(reporting_period.date_time)
159
- Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit] - 1, Saulabs::Reportable::ReportCache.new), cached)
142
+ Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return(Array.new(@report.options[:limit] - 1, Saulabs::Reportable::ReportCache.new), cached)
160
143
 
161
144
  Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at|
162
145
  begin_at.should == reporting_period.date_time
@@ -170,7 +153,7 @@ describe Saulabs::Reportable::ReportCache do
170
153
  describe 'with :live_data = false' do
171
154
 
172
155
  it 'should not yield if all required data could be retrieved from the cache' do
173
- Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit], Saulabs::Reportable::ReportCache.new))
156
+ Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return(Array.new(@report.options[:limit], Saulabs::Reportable::ReportCache.new))
174
157
 
175
158
  lambda {
176
159
  Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new }
@@ -178,7 +161,7 @@ describe Saulabs::Reportable::ReportCache do
178
161
  end
179
162
 
180
163
  it 'should yield to the block if no data could be retrieved from the cache' do
181
- Saulabs::Reportable::ReportCache.stub!(:all).and_return([])
164
+ Saulabs::Reportable::ReportCache.stub!(:read_cached_data).and_return([])
182
165
 
183
166
  lambda {
184
167
  Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new }
@@ -188,7 +171,7 @@ describe Saulabs::Reportable::ReportCache do
188
171
  describe 'with :end_date = <some date>' do
189
172
 
190
173
  before do
191
- @options = @report.options.merge(:end_date => Time.now)
174
+ @options = @report.options.merge(:end_date => Time.now - 1.send(@report.options[:grouping].identifier))
192
175
  end
193
176
 
194
177
  it 'should yield the last date and time of the reporting period for the specified end date' do
@@ -204,7 +187,7 @@ describe Saulabs::Reportable::ReportCache do
204
187
 
205
188
  end
206
189
 
207
- it 'should read existing data from the cache' do
190
+ xit 'should read existing data from the cache' do
208
191
  Saulabs::Reportable::ReportCache.should_receive(:all).once.with(
209
192
  :conditions => [
210
193
  %w(model_name report_name grouping aggregation conditions).map do |column_name|
@@ -224,8 +207,8 @@ describe Saulabs::Reportable::ReportCache do
224
207
  Saulabs::Reportable::ReportCache.process(@report, @report.options) { [] }
225
208
  end
226
209
 
227
- it 'should utilize the end_date in the conditions' do
228
- end_date = Time.now
210
+ xit 'should utilize the end_date in the conditions' do
211
+ end_date = Time.now - 1.send(@report.options[:grouping].identifier)
229
212
  Saulabs::Reportable::ReportCache.should_receive(:all).once.with(
230
213
  :conditions => [
231
214
  %w(model_name report_name grouping aggregation conditions).map do |column_name|
@@ -236,7 +219,7 @@ describe Saulabs::Reportable::ReportCache do
236
219
  @report.options[:grouping].identifier.to_s,
237
220
  @report.aggregation.to_s,
238
221
  '',
239
- Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], 9).date_time,
222
+ Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], 10).date_time,
240
223
  Saulabs::Reportable::ReportingPeriod.new(@report.options[:grouping], end_date).date_time
241
224
  ],
242
225
  :limit => 10,
@@ -246,7 +229,7 @@ describe Saulabs::Reportable::ReportCache do
246
229
  Saulabs::Reportable::ReportCache.process(@report, @report.options.merge(:end_date => end_date)) { [] }
247
230
  end
248
231
 
249
- it "should read existing data from the cache for the correct grouping if one other than the report's default grouping is specified" do
232
+ xit "should read existing data from the cache for the correct grouping if one other than the report's default grouping is specified" do
250
233
  grouping = Saulabs::Reportable::Grouping.new(:month)
251
234
  Saulabs::Reportable::ReportCache.should_receive(:all).once.with(
252
235
  :conditions => [
@@ -275,7 +258,15 @@ describe Saulabs::Reportable::ReportCache do
275
258
  end
276
259
  end
277
260
  end
278
-
261
+
262
+ describe '.get_first_reporting_period_to_read' do
263
+ it 'returns first reporting period if no cached data' do
264
+ Saulabs::Reportable::ReportCache.should_receive(:get_first_reporting_period).once.and_return('first')
265
+ result = Saulabs::Reportable::ReportCache.send(:get_first_reporting_period_to_read, [], {})
266
+ result.should == 'first'
267
+ end
268
+ end
269
+
279
270
  describe '.serialize_conditions' do
280
271
 
281
272
  it 'should serialize empty conditions correctly' do
@@ -21,7 +21,7 @@ describe Saulabs::Reportable::Report do
21
21
  it 'should process the data with the report cache' do
22
22
  Saulabs::Reportable::ReportCache.should_receive(:process).once.with(
23
23
  @report,
24
- { :limit => 100, :grouping => @report.options[:grouping], :conditions => [], :live_data => false, :end_date => false }
24
+ { :limit => 100, :grouping => @report.options[:grouping], :conditions => [], :include => [], :live_data => false, :end_date => false, :distinct => false }
25
25
  )
26
26
 
27
27
  @report.run
@@ -30,7 +30,7 @@ describe Saulabs::Reportable::Report do
30
30
  it 'should process the data with the report cache when custom conditions are given' do
31
31
  Saulabs::Reportable::ReportCache.should_receive(:process).once.with(
32
32
  @report,
33
- { :limit => 100, :grouping => @report.options[:grouping], :conditions => { :some => :condition }, :live_data => false, :end_date => false }
33
+ { :limit => 100, :grouping => @report.options[:grouping], :conditions => { :some => :condition }, :include => [], :live_data => false, :end_date => false, :distinct => false }
34
34
  )
35
35
 
36
36
  @report.run(:conditions => { :some => :condition })
@@ -47,7 +47,7 @@ describe Saulabs::Reportable::Report do
47
47
  Saulabs::Reportable::Grouping.should_receive(:new).once.with(:month).and_return(grouping)
48
48
  Saulabs::Reportable::ReportCache.should_receive(:process).once.with(
49
49
  @report,
50
- { :limit => 100, :grouping => grouping, :conditions => [], :live_data => false, :end_date => false }
50
+ { :limit => 100, :grouping => grouping, :conditions => [], :live_data => false, :end_date => false, :distinct => false, :include => [] }
51
51
  )
52
52
 
53
53
  @report.run(:grouping => :month)
@@ -65,15 +65,152 @@ describe Saulabs::Reportable::Report do
65
65
  @report.run.to_a.length.should == 11
66
66
  end
67
67
 
68
- for grouping in [:hour, :day, :week, :month] do
68
+ %w(hour day week month).each do |grouping|
69
+ grouping = grouping.to_sym
69
70
 
70
71
  describe "for grouping :#{grouping.to_s}" do
71
72
 
72
73
  before(:all) do
73
- User.create!(:login => 'test 1', :created_at => Time.now, :profile_visits => 2)
74
- User.create!(:login => 'test 2', :created_at => Time.now - 1.send(grouping), :profile_visits => 1)
75
- User.create!(:login => 'test 3', :created_at => Time.now - 3.send(grouping), :profile_visits => 2)
76
- User.create!(:login => 'test 4', :created_at => Time.now - 3.send(grouping), :profile_visits => 3)
74
+ User.create!(:login => 'test 1', :created_at => Time.now, :profile_visits => 2, :sub_type => "red")
75
+ User.create!(:login => 'test 2', :created_at => Time.now - 1.send(grouping), :profile_visits => 1, :sub_type => "red")
76
+ User.create!(:login => 'test 3', :created_at => Time.now - 3.send(grouping), :profile_visits => 2, :sub_type => "blue")
77
+ User.create!(:login => 'test 4', :created_at => Time.now - 3.send(grouping), :profile_visits => 3, :sub_type => "blue")
78
+ end
79
+
80
+ describe 'optimized querying with contiguously cached data' do
81
+ it "should be optimized with specified end_date" do
82
+ @end_date = DateTime.now - 1.send(grouping)
83
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
84
+ :grouping => grouping,
85
+ :limit => 10,
86
+ :end_date => @end_date
87
+ )
88
+ @result = @report.run
89
+
90
+ Saulabs::Reportable::ReportCache.last.delete
91
+
92
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
93
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, @end_date)
94
+
95
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
96
+ begin_at.should == reporting_period.date_time
97
+ end_at.should == reporting_period.last_date_time
98
+ [] # without this rspec whines about an ambiguous return value
99
+ end
100
+
101
+ @result = @report.run
102
+ end
103
+
104
+ it "should be optimized without specific end_date and live_data" do
105
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
106
+ :grouping => grouping,
107
+ :limit => 10,
108
+ :live_data => true
109
+ )
110
+ @result = @report.run.to_a
111
+
112
+ Saulabs::Reportable::ReportCache.last.delete
113
+
114
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
115
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, DateTime.now).previous
116
+
117
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
118
+ begin_at.should == reporting_period.date_time
119
+ end_at.should == nil
120
+ [] # without this rspec whines about an ambiguous return value
121
+ end
122
+
123
+ @result = @report.run
124
+ end
125
+
126
+ it "should be optimized without specific end_date and without live_data requested" do
127
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
128
+ :grouping => grouping,
129
+ :limit => 10
130
+ )
131
+ @result = @report.run.to_a
132
+
133
+ Saulabs::Reportable::ReportCache.last.delete
134
+
135
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
136
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, DateTime.now).previous
137
+
138
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
139
+ begin_at.should == reporting_period.date_time
140
+ end_at.should == nil
141
+ [] # without this rspec whines about an ambiguous return value
142
+ end
143
+
144
+ @result = @report.run
145
+ end
146
+ end
147
+
148
+ describe 'non optimized querying when gaps present in cached data' do
149
+ it "should not be optimized with specified end_date" do
150
+ @end_date = DateTime.now - 1.send(grouping)
151
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
152
+ :grouping => grouping,
153
+ :limit => 10,
154
+ :end_date => @end_date
155
+ )
156
+ @result = @report.run
157
+
158
+ Saulabs::Reportable::ReportCache.first.delete
159
+
160
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
161
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, @end_date)
162
+
163
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
164
+ begin_at.should == reporting_period.offset(-9).date_time
165
+ end_at.should == reporting_period.last_date_time
166
+ [] # without this rspec whines about an ambiguous return value
167
+ end
168
+
169
+ @result = @report.run
170
+ end
171
+
172
+ it "should not be optimized without specific end_date and live_data" do
173
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
174
+ :grouping => grouping,
175
+ :limit => 10,
176
+ :live_data => true
177
+ )
178
+ @result = @report.run.to_a
179
+
180
+ Saulabs::Reportable::ReportCache.first.delete
181
+
182
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
183
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, DateTime.now).previous
184
+
185
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
186
+ begin_at.should == reporting_period.offset(-9).date_time
187
+ end_at.should == nil
188
+ [] # without this rspec whines about an ambiguous return value
189
+ end
190
+
191
+ @result = @report.run
192
+ end
193
+
194
+ it "should not be optimized without specific end_date and without live_data requested" do
195
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
196
+ :grouping => grouping,
197
+ :limit => 10
198
+ )
199
+ @result = @report.run.to_a
200
+
201
+ Saulabs::Reportable::ReportCache.first.delete
202
+
203
+ grouping_instance = Saulabs::Reportable::Grouping.new(grouping)
204
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(grouping_instance, DateTime.now).previous
205
+
206
+ @report.should_receive(:read_data) do |begin_at, end_at, options|
207
+ begin_at.should == reporting_period.offset(-9).date_time
208
+ end_at.should == nil
209
+ [] # without this rspec whines about an ambiguous return value
210
+ end
211
+
212
+ @result = @report.run
213
+ end
77
214
  end
78
215
 
79
216
  describe 'when :end_date is specified' do
@@ -162,6 +299,24 @@ describe Saulabs::Reportable::Report do
162
299
  result[6][1].should == 0.0
163
300
  end
164
301
 
302
+ it 'should return correct data for aggregation :count with distinct: true' do
303
+ @report = Saulabs::Reportable::Report.new(User, :registrations,
304
+ :aggregation => :count,
305
+ :grouping => grouping,
306
+ :value_column => :sub_type,
307
+ :distinct => true,
308
+ :limit => 10,
309
+ :live_data => live_data
310
+ )
311
+ result = @report.run.to_a
312
+
313
+ result[10][1].should == 1.0 if live_data
314
+ result[9][1].should == 1.0
315
+ result[8][1].should == 0.0
316
+ result[7][1].should == 1.0
317
+ result[6][1].should == 0.0
318
+ end
319
+
165
320
  it 'should return correct data for aggregation :sum' do
166
321
  @report = Saulabs::Reportable::Report.new(User, :registrations,
167
322
  :aggregation => :sum,
@@ -426,7 +581,7 @@ describe Saulabs::Reportable::Report do
426
581
 
427
582
  describe '#read_data' do
428
583
 
429
- it 'should invoke the aggregation method on the model' do
584
+ xit 'should invoke the aggregation method on the model' do
430
585
  @report = Saulabs::Reportable::Report.new(User, :registrations, :aggregation => :count)
431
586
  User.should_receive(:count).once.and_return([])
432
587
 
@@ -4,6 +4,7 @@ ActiveRecord::Schema.define(:version => 1) do
4
4
  t.string :login, :null => false
5
5
  t.integer :profile_visits, :null => false, :default => 0
6
6
  t.string :type, :null => false, :default => 'User'
7
+ t.string :sub_type
7
8
 
8
9
  t.timestamps
9
10
  end
@@ -27,8 +27,14 @@ require File.join(ROOT, 'lib', 'saulabs', 'reportable.rb')
27
27
  # config.time_zone = 'Pacific Time (US & Canada)'
28
28
  # end
29
29
 
30
- FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log')
31
- ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/log/spec.log")
30
+ # FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log')
31
+ # ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/log/spec.log")
32
+
33
+ RSpec.configure do |config|
34
+ config.filter_run :focus => true
35
+ config.run_all_when_everything_filtered = true
36
+ end
37
+ ActiveRecord::Base.default_timezone = :local
32
38
 
33
39
  databases = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml')))
34
40
  ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'sqlite3'])
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reportable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
5
- prerelease:
4
+ version: 1.3.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Marco Otte-Witte
@@ -14,26 +13,32 @@ date: 2012-02-14 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: activerecord
17
- requirement: &70168900389180 !ruby/object:Gem::Requirement
18
- none: false
16
+ requirement: !ruby/object:Gem::Requirement
19
17
  requirements:
20
- - - ! '>='
18
+ - - ">="
21
19
  - !ruby/object:Gem::Version
22
20
  version: '3.0'
23
21
  type: :runtime
24
22
  prerelease: false
25
- version_requirements: *70168900389180
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
26
28
  - !ruby/object:Gem::Dependency
27
29
  name: activesupport
28
- requirement: &70168900388200 !ruby/object:Gem::Requirement
29
- none: false
30
+ requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - ! '>='
32
+ - - ">="
32
33
  - !ruby/object:Gem::Version
33
34
  version: 3.0.0
34
35
  type: :runtime
35
36
  prerelease: false
36
- version_requirements: *70168900388200
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.0
37
42
  description: Reportable allows for easy report generation from ActiveRecord models
38
43
  by the addition of the reportable method.
39
44
  email: reportable@saulabs.com
@@ -41,21 +46,22 @@ executables: []
41
46
  extensions: []
42
47
  extra_rdoc_files: []
43
48
  files:
44
- - README.md
45
49
  - HISTORY.md
46
- - Rakefile
47
50
  - MIT-LICENSE
51
+ - README.md
52
+ - Rakefile
48
53
  - generators/reportable_jquery_flot_assets/reportable_jquery_flot_assets_generator.rb
54
+ - generators/reportable_jquery_flot_assets/templates/NOTES
49
55
  - generators/reportable_jquery_flot_assets/templates/excanvas.min.js
50
56
  - generators/reportable_jquery_flot_assets/templates/jquery.flot.min.js
51
- - generators/reportable_jquery_flot_assets/templates/NOTES
52
57
  - generators/reportable_migration/reportable_migration_generator.rb
53
58
  - generators/reportable_migration/templates/migration.rb
54
59
  - generators/reportable_raphael_assets/reportable_raphael_assets_generator.rb
60
+ - generators/reportable_raphael_assets/templates/NOTES
55
61
  - generators/reportable_raphael_assets/templates/g.line.min.js
56
62
  - generators/reportable_raphael_assets/templates/g.raphael.min.js
57
- - generators/reportable_raphael_assets/templates/NOTES
58
63
  - generators/reportable_raphael_assets/templates/raphael.min.js
64
+ - lib/saulabs/reportable.rb
59
65
  - lib/saulabs/reportable/config.rb
60
66
  - lib/saulabs/reportable/cumulated_report.rb
61
67
  - lib/saulabs/reportable/grouping.rb
@@ -65,41 +71,39 @@ files:
65
71
  - lib/saulabs/reportable/report_tag_helper.rb
66
72
  - lib/saulabs/reportable/reporting_period.rb
67
73
  - lib/saulabs/reportable/result_set.rb
68
- - lib/saulabs/reportable.rb
69
74
  - spec/classes/cumulated_report_spec.rb
70
75
  - spec/classes/grouping_spec.rb
71
76
  - spec/classes/report_cache_spec.rb
72
77
  - spec/classes/report_spec.rb
73
78
  - spec/classes/reporting_period_spec.rb
79
+ - spec/db/database.yml
74
80
  - spec/db/schema.rb
75
81
  - spec/other/report_method_spec.rb
76
82
  - spec/other/report_tag_helper_spec.rb
77
- - spec/spec_helper.rb
78
- - spec/db/database.yml
79
83
  - spec/spec.opts
84
+ - spec/spec_helper.rb
80
85
  homepage: http://github.com/saulabs/reportable
81
86
  licenses: []
87
+ metadata: {}
82
88
  post_install_message:
83
89
  rdoc_options: []
84
90
  require_paths:
85
91
  - lib
86
92
  required_ruby_version: !ruby/object:Gem::Requirement
87
- none: false
88
93
  requirements:
89
- - - ! '>='
94
+ - - ">="
90
95
  - !ruby/object:Gem::Version
91
96
  version: '0'
92
97
  required_rubygems_version: !ruby/object:Gem::Requirement
93
- none: false
94
98
  requirements:
95
- - - ! '>='
99
+ - - ">="
96
100
  - !ruby/object:Gem::Version
97
101
  version: '0'
98
102
  requirements: []
99
103
  rubyforge_project:
100
- rubygems_version: 1.8.11
104
+ rubygems_version: 2.2.2
101
105
  signing_key:
102
- specification_version: 3
106
+ specification_version: 4
103
107
  summary: Easy report generation for Ruby on Rails
104
108
  test_files: []
105
109
  has_rdoc: false