reportable 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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