reportable 1.0.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.
- data/HISTORY.md +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +88 -0
- data/Rakefile +29 -0
- data/generators/reportable_migration/reportable_migration_generator.rb +9 -0
- data/generators/reportable_migration/templates/migration.erb +40 -0
- data/lib/saulabs/reportable.rb +63 -0
- data/lib/saulabs/reportable/cumulated_report.rb +42 -0
- data/lib/saulabs/reportable/grouping.rb +140 -0
- data/lib/saulabs/reportable/report.rb +165 -0
- data/lib/saulabs/reportable/report_cache.rb +163 -0
- data/lib/saulabs/reportable/reporting_period.rb +181 -0
- data/lib/saulabs/reportable/sparkline_tag_helper.rb +62 -0
- data/rails/init.rb +9 -0
- data/spec/boot.rb +25 -0
- data/spec/classes/cumulated_report_spec.rb +169 -0
- data/spec/classes/grouping_spec.rb +155 -0
- data/spec/classes/report_cache_spec.rb +296 -0
- data/spec/classes/report_spec.rb +574 -0
- data/spec/classes/reporting_period_spec.rb +335 -0
- data/spec/db/database.yml +15 -0
- data/spec/db/schema.rb +38 -0
- data/spec/other/report_method_spec.rb +44 -0
- data/spec/other/sparkline_tag_helper_spec.rb +64 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +23 -0
- metadata +100 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# The +ReportCache+ class is a regular +ActiveRecord+ model and represents cached results for single {Saulabs::Reportable::ReportingPeriod}s.
|
6
|
+
# +ReportCache+ instances are identified by the combination of +model_name+, +report_name+, +grouping+, +aggregation+ and +reporting_period+.
|
7
|
+
#
|
8
|
+
class ReportCache < ActiveRecord::Base
|
9
|
+
|
10
|
+
set_table_name :reportable_cache
|
11
|
+
|
12
|
+
self.skip_time_zone_conversion_for_attributes = [:reporting_period]
|
13
|
+
|
14
|
+
# Clears the cache for the specified +klass+ and +report+
|
15
|
+
#
|
16
|
+
# @param [Class] klass
|
17
|
+
# the model the report to clear the cache for works on
|
18
|
+
# @param [Symbol] report
|
19
|
+
# the name of the report to clear the cache for
|
20
|
+
#
|
21
|
+
# @example Clearing the cache for a report
|
22
|
+
#
|
23
|
+
# class User < ActiveRecord::Base
|
24
|
+
# reportable :registrations
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Saulabs::Reportable::ReportCache.clear_for(User, :registrations)
|
28
|
+
#
|
29
|
+
def self.clear_for(klass, report)
|
30
|
+
self.delete_all(:conditions => {
|
31
|
+
:model_name => klass.name,
|
32
|
+
:report_name => report.to_s
|
33
|
+
})
|
34
|
+
end
|
35
|
+
|
36
|
+
# Processes the report using the respective cache.
|
37
|
+
#
|
38
|
+
# @param [Saulabe::Reportable::Report] report
|
39
|
+
# the report to process
|
40
|
+
# @param [Hash] options
|
41
|
+
# options for the report
|
42
|
+
#
|
43
|
+
# @option options [Symbol] :grouping (:day)
|
44
|
+
# the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
|
45
|
+
# @option options [Fixnum] :limit (100)
|
46
|
+
# the number of reporting periods to get (see +:grouping+)
|
47
|
+
# @option options [Hash] :conditions ({})
|
48
|
+
# conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
|
49
|
+
# @option options [Boolean] :live_data (false)
|
50
|
+
# 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>
|
51
|
+
# @option options [DateTime, Boolean] :end_date (false)
|
52
|
+
# when specified, the report will only include data for the +:limit+ reporting periods until this date.
|
53
|
+
#
|
54
|
+
# @return [Array<Array<DateTime, Float>>]
|
55
|
+
# the result of the report as pairs of {DateTime}s and {Float}s
|
56
|
+
#
|
57
|
+
def self.process(report, options, &block)
|
58
|
+
raise ArgumentError.new('A block must be given') unless block_given?
|
59
|
+
self.transaction do
|
60
|
+
cached_data = read_cached_data(report, options)
|
61
|
+
new_data = read_new_data(cached_data, options, &block)
|
62
|
+
prepare_result(new_data, cached_data, report, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def self.prepare_result(new_data, cached_data, report, options)
|
69
|
+
new_data = new_data.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] }
|
70
|
+
cached_data.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] }
|
71
|
+
current_reporting_period = ReportingPeriod.new(options[:grouping])
|
72
|
+
reporting_period = get_first_reporting_period(options)
|
73
|
+
result = []
|
74
|
+
while reporting_period < (options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).next : current_reporting_period)
|
75
|
+
if cached = cached_data.find { |cached| reporting_period == cached[0] }
|
76
|
+
result << [cached[0].date_time, cached[1]]
|
77
|
+
else
|
78
|
+
new_cached = build_cached_data(report, options[:grouping], options[:conditions], reporting_period, find_value(new_data, reporting_period))
|
79
|
+
new_cached.save!
|
80
|
+
result << [reporting_period.date_time, new_cached.value]
|
81
|
+
end
|
82
|
+
reporting_period = reporting_period.next
|
83
|
+
end
|
84
|
+
if options[:live_data]
|
85
|
+
result << [current_reporting_period.date_time, find_value(new_data, current_reporting_period)]
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.find_value(data, reporting_period)
|
91
|
+
data = data.detect { |d| d[0] == reporting_period }
|
92
|
+
data ? data[1] : 0.0
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.build_cached_data(report, grouping, condition, reporting_period, value)
|
96
|
+
self.new(
|
97
|
+
:model_name => report.klass.to_s,
|
98
|
+
:report_name => report.name.to_s,
|
99
|
+
:grouping => grouping.identifier.to_s,
|
100
|
+
:aggregation => report.aggregation.to_s,
|
101
|
+
:condition => condition.to_s,
|
102
|
+
:reporting_period => reporting_period.date_time,
|
103
|
+
:value => value
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.read_cached_data(report, options)
|
108
|
+
conditions = [
|
109
|
+
'model_name = ? AND report_name = ? AND grouping = ? AND aggregation = ? AND `condition` = ?',
|
110
|
+
report.klass.to_s,
|
111
|
+
report.name.to_s,
|
112
|
+
options[:grouping].identifier.to_s,
|
113
|
+
report.aggregation.to_s,
|
114
|
+
options[:conditions].to_s
|
115
|
+
]
|
116
|
+
first_reporting_period = get_first_reporting_period(options)
|
117
|
+
last_reporting_period = get_last_reporting_period(options)
|
118
|
+
if last_reporting_period
|
119
|
+
conditions.first << ' AND reporting_period BETWEEN ? AND ?'
|
120
|
+
conditions << first_reporting_period.date_time
|
121
|
+
conditions << last_reporting_period.date_time
|
122
|
+
else
|
123
|
+
conditions.first << ' AND reporting_period >= ?'
|
124
|
+
conditions << first_reporting_period.date_time
|
125
|
+
end
|
126
|
+
self.all(
|
127
|
+
:conditions => conditions,
|
128
|
+
:limit => options[:limit],
|
129
|
+
:order => 'reporting_period ASC'
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.read_new_data(cached_data, options, &block)
|
134
|
+
if !options[:live_data] && cached_data.length == options[:limit]
|
135
|
+
[]
|
136
|
+
else
|
137
|
+
first_reporting_period_to_read = if cached_data.length < options[:limit]
|
138
|
+
get_first_reporting_period(options)
|
139
|
+
else
|
140
|
+
ReportingPeriod.new(options[:grouping], cached_data.last.reporting_period).next
|
141
|
+
end
|
142
|
+
last_reporting_period_to_read = options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).last_date_time : nil
|
143
|
+
yield(first_reporting_period_to_read.date_time, last_reporting_period_to_read)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.get_first_reporting_period(options)
|
148
|
+
if options[:end_date]
|
149
|
+
ReportingPeriod.first(options[:grouping], options[:limit] - 1, options[:end_date])
|
150
|
+
else
|
151
|
+
ReportingPeriod.first(options[:grouping], options[:limit])
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.get_last_reporting_period(options)
|
156
|
+
return ReportingPeriod.new(options[:grouping], options[:end_date]) if options[:end_date]
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# A reporting period is a specific hour or a specific day etc. depending on the used {Saulabs::Reportable::Grouping}.
|
6
|
+
#
|
7
|
+
class ReportingPeriod
|
8
|
+
|
9
|
+
# The actual +DateTime the reporting period represents
|
10
|
+
#
|
11
|
+
attr_reader :date_time
|
12
|
+
|
13
|
+
# The {Saulabs::Reportable::Grouping} of the reporting period
|
14
|
+
#
|
15
|
+
attr_reader :grouping
|
16
|
+
|
17
|
+
# Initializes a new reporting period.
|
18
|
+
#
|
19
|
+
# @param [Saulabs::Reportable::Grouping] grouping
|
20
|
+
# the grouping the generate the reporting period for
|
21
|
+
# @param [DateTime] date_time
|
22
|
+
# the +DateTime+ to generate the reporting period for
|
23
|
+
#
|
24
|
+
def initialize(grouping, date_time = nil)
|
25
|
+
@grouping = grouping
|
26
|
+
@date_time = parse_date_time(date_time || DateTime.now)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gets a reporting period relative to the current one.
|
30
|
+
#
|
31
|
+
# @param [Fixnum] offset
|
32
|
+
# the offset to get the reporting period for
|
33
|
+
#
|
34
|
+
# @return [Saulabs::Reportable::ReportingPeriod]
|
35
|
+
# the reporting period relative by offset to the current one
|
36
|
+
#
|
37
|
+
# @example Getting the reporting period one week later
|
38
|
+
#
|
39
|
+
# reporting_period = Saulabs::Reportable::ReportingPeriod.new(:week, DateTime.now)
|
40
|
+
# next_week = reporting_period.offset(1)
|
41
|
+
#
|
42
|
+
def offset(offset)
|
43
|
+
self.class.new(@grouping, @date_time + offset.send(@grouping.identifier))
|
44
|
+
end
|
45
|
+
|
46
|
+
# Gets the first reporting period for a grouping and a limit (optionally relative to and end date).
|
47
|
+
#
|
48
|
+
# @param [Saulabs::ReportingPeriod::Grouping] grouping
|
49
|
+
# the grouping to get the first reporting period for
|
50
|
+
# @param [Fixnum] limit
|
51
|
+
# the limit to get the first reporting period for
|
52
|
+
# @param [DateTime] end_date
|
53
|
+
# the end date to get the first reporting period for (the first reporting period is then +end_date+ - +limit+ * +grouping+)
|
54
|
+
#
|
55
|
+
# @return [Saulabs::Reportable::ReportingPeriod]
|
56
|
+
# the first reporting period for the grouping, limit and optionally end date
|
57
|
+
#
|
58
|
+
def self.first(grouping, limit, end_date = nil)
|
59
|
+
self.new(grouping, end_date).offset(-limit)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Gets a reporting period from a DB date string.
|
63
|
+
#
|
64
|
+
# @param [Saulabs::Reportable::Grouping] grouping
|
65
|
+
# the grouping to get the reporting period for
|
66
|
+
# @param [String] db_string
|
67
|
+
# the DB string to parse and get the reporting period for
|
68
|
+
#
|
69
|
+
# @return [Saulabs::Reportable::ReportingPeriod]
|
70
|
+
# the reporting period for the {Saulabs::Reportable::Grouping} as parsed from the db string
|
71
|
+
#
|
72
|
+
def self.from_db_string(grouping, db_string)
|
73
|
+
parts = grouping.date_parts_from_db_string(db_string)
|
74
|
+
result = case grouping.identifier
|
75
|
+
when :hour
|
76
|
+
self.new(grouping, DateTime.new(parts[0], parts[1], parts[2], parts[3], 0, 0))
|
77
|
+
when :day
|
78
|
+
self.new(grouping, Date.new(parts[0], parts[1], parts[2]))
|
79
|
+
when :week
|
80
|
+
self.new(grouping, Date.commercial(parts[0], parts[1], 1))
|
81
|
+
when :month
|
82
|
+
self.new(grouping, Date.new(parts[0], parts[1], 1))
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
# Gets the next reporting period.
|
88
|
+
#
|
89
|
+
# @return [Saulabs::Reportable::ReportingPeriod]
|
90
|
+
# the reporting period after the current one
|
91
|
+
#
|
92
|
+
def next
|
93
|
+
self.offset(1)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Gets the previous reporting period.
|
97
|
+
#
|
98
|
+
# @return [Saulabs::Reportable::ReportingPeriod]
|
99
|
+
# the reporting period before the current one
|
100
|
+
#
|
101
|
+
def previous
|
102
|
+
self.offset(-1)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Gets whether the reporting period +other+ is equal to the current one.
|
106
|
+
#
|
107
|
+
# @param [Saulabs::Reportable::ReportingPeriod] other
|
108
|
+
# the reporting period to check for whether it is equal to the current one
|
109
|
+
#
|
110
|
+
# @return [Boolean]
|
111
|
+
# true if +other+ is equal to the current reporting period, false otherwise
|
112
|
+
#
|
113
|
+
def ==(other)
|
114
|
+
if other.is_a?(Saulabs::Reportable::ReportingPeriod)
|
115
|
+
@date_time.to_s == other.date_time.to_s && @grouping.identifier.to_s == other.grouping.identifier.to_s
|
116
|
+
elsif other.is_a?(Time) || other.is_a?(DateTime)
|
117
|
+
@date_time == parse_date_time(other)
|
118
|
+
else
|
119
|
+
raise ArgumentError.new("Can only compare instances of #{self.class.name}")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Gets whether the reporting period +other+ is smaller to the current one.
|
124
|
+
#
|
125
|
+
# @param [Saulabs::Reportable::ReportingPeriod] other
|
126
|
+
# the reporting period to check for whether it is smaller to the current one
|
127
|
+
#
|
128
|
+
# @return [Boolean]
|
129
|
+
# true if +other+ is smaller to the current reporting period, false otherwise
|
130
|
+
#
|
131
|
+
def <(other)
|
132
|
+
if other.is_a?(Saulabs::Reportable::ReportingPeriod)
|
133
|
+
return @date_time < other.date_time
|
134
|
+
elsif other.is_a?(Time) || other.is_a?(DateTime)
|
135
|
+
@date_time < parse_date_time(other)
|
136
|
+
else
|
137
|
+
raise ArgumentError.new("Can only compare instances of #{self.class.name}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Gets the latest point in time that is included the reporting period. The latest point in time included in a reporting period
|
142
|
+
# for grouping hour would be that hour and 59 minutes and 59 seconds.
|
143
|
+
#
|
144
|
+
# @return [DateTime]
|
145
|
+
# the latest point in time that is included in the reporting period
|
146
|
+
#
|
147
|
+
def last_date_time
|
148
|
+
case @grouping.identifier
|
149
|
+
when :hour
|
150
|
+
DateTime.new(@date_time.year, @date_time.month, @date_time.day, @date_time.hour, 59, 59)
|
151
|
+
when :day
|
152
|
+
DateTime.new(@date_time.year, @date_time.month, @date_time.day, 23, 59, 59)
|
153
|
+
when :week
|
154
|
+
date_time = (@date_time - @date_time.wday.days) + 7.days
|
155
|
+
Date.new(date_time.year, date_time.month, date_time.day)
|
156
|
+
when :month
|
157
|
+
Date.new(@date_time.year, @date_time.month, (Date.new(@date_time.year, 12, 31) << (12 - @date_time.month)).day)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def parse_date_time(date_time)
|
164
|
+
case @grouping.identifier
|
165
|
+
when :hour
|
166
|
+
DateTime.new(date_time.year, date_time.month, date_time.day, date_time.hour)
|
167
|
+
when :day
|
168
|
+
date_time.to_date
|
169
|
+
when :week
|
170
|
+
date_time = (date_time - date_time.wday.days) + 1.day
|
171
|
+
Date.new(date_time.year, date_time.month, date_time.day)
|
172
|
+
when :month
|
173
|
+
Date.new(date_time.year, date_time.month, 1)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
module SparklineTagHelper
|
6
|
+
|
7
|
+
# Renders a sparkline with the given data.
|
8
|
+
#
|
9
|
+
# @param [Array<Array<DateTime, Float>>] data
|
10
|
+
# an array of report data as returned by {Saulabs::Reportable::Report#run}
|
11
|
+
# @param [Hash] options
|
12
|
+
# options for the sparkline
|
13
|
+
#
|
14
|
+
# @option options [Fixnum] :width (300)
|
15
|
+
# the width of the generated image
|
16
|
+
# @option options [Fixnum] :height (34)
|
17
|
+
# the height of the generated image
|
18
|
+
# @option options [String] :line_color ('0077cc')
|
19
|
+
# the line color of the generated image
|
20
|
+
# @option options [String] :fill_color ('e6f2fa')
|
21
|
+
# the fill color of the generated image
|
22
|
+
# @option options [Array<Symbol>] :labels ([])
|
23
|
+
# the axes to render lables for (Array of +:x+, +:y+, +:r+, +:t+; this is x axis, y axis, right, top)
|
24
|
+
# @option options [String] :alt ('')
|
25
|
+
# the alt attribute for the generated image
|
26
|
+
# @option options [String] :title ('')
|
27
|
+
# the title attribute for the generated image
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
# an image tag showing a sparkline for the passed +data+
|
31
|
+
#
|
32
|
+
# @example Rendering a sparkline tag for report data
|
33
|
+
#
|
34
|
+
# <%= sparkline_tag(User.registrations_report, :width => 200, :height => 100, :color => '000') %>
|
35
|
+
#
|
36
|
+
def sparkline_tag(data, options = {})
|
37
|
+
options.reverse_merge!({ :width => 300, :height => 34, :line_color => '0077cc', :fill_color => 'e6f2fa', :labels => [], :alt => '', :title => '' })
|
38
|
+
data = data.collect { |d| d[1] }
|
39
|
+
labels = ''
|
40
|
+
unless options[:labels].empty?
|
41
|
+
chxr = {}
|
42
|
+
options[:labels].each_with_index do |l, i|
|
43
|
+
chxr[l] = "#{i}," + ([:x, :t].include?(l) ? "0,#{data.length}" : "#{[data.min, 0].min},#{data.max}")
|
44
|
+
end
|
45
|
+
labels = "&chxt=#{options[:labels].map(&:to_s).join(',')}&chxr=#{options[:labels].collect{|l| chxr[l]}.join('|')}"
|
46
|
+
end
|
47
|
+
title = ''
|
48
|
+
unless options[:title].empty?
|
49
|
+
title = "&chtt=#{options[:title]}"
|
50
|
+
end
|
51
|
+
image_tag(
|
52
|
+
"http://chart.apis.google.com/chart?cht=ls&chs=#{options[:width]}x#{options[:height]}&chd=t:#{data.join(',')}&chco=#{options[:line_color]}&chm=B,#{options[:fill_color]},0,0,0&chls=1,0,0&chds=#{data.min},#{data.max}#{labels}#{title}",
|
53
|
+
:alt => options[:alt],
|
54
|
+
:title => options[:title]
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/rails/init.rb
ADDED
data/spec/boot.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
plugin_root = File.join(File.dirname(__FILE__), '..')
|
2
|
+
|
3
|
+
gem 'rails'
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_support'
|
6
|
+
require 'action_controller'
|
7
|
+
require 'action_view'
|
8
|
+
|
9
|
+
$:.unshift "#{plugin_root}/lib"
|
10
|
+
|
11
|
+
RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
|
12
|
+
Rails::Initializer.run(:set_load_path)
|
13
|
+
Rails::Initializer.run(:set_autoload_paths)
|
14
|
+
Rails::Initializer.run(:initialize_time_zone) do |config|
|
15
|
+
config.time_zone = 'Pacific Time (US & Canada)'
|
16
|
+
end
|
17
|
+
|
18
|
+
require File.join(File.dirname(__FILE__), '..', 'rails', 'init.rb')
|
19
|
+
|
20
|
+
FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log')
|
21
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'log', 'spec.log'))
|
22
|
+
|
23
|
+
databases = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml')))
|
24
|
+
ActiveRecord::Base.establish_connection(databases['sqlite3'])
|
25
|
+
load(File.join(File.dirname(__FILE__), 'db', 'schema.rb'))
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Saulabs::Reportable::CumulatedReport do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#run' do
|
10
|
+
|
11
|
+
it 'should cumulate the data' do
|
12
|
+
@report.should_receive(:cumulate).once
|
13
|
+
|
14
|
+
@report.run
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return an array of the same length as the specified limit when :live_data is false' do
|
18
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations, :limit => 10, :live_data => false)
|
19
|
+
|
20
|
+
@report.run.length.should == 10
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should return an array of the same length as the specified limit + 1 when :live_data is true' do
|
24
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations, :limit => 10, :live_data => true)
|
25
|
+
|
26
|
+
@report.run.length.should == 11
|
27
|
+
end
|
28
|
+
|
29
|
+
for grouping in [:hour, :day, :week, :month] do
|
30
|
+
|
31
|
+
describe "for grouping #{grouping.to_s}" do
|
32
|
+
|
33
|
+
[true, false].each do |live_data|
|
34
|
+
|
35
|
+
describe "with :live_data = #{live_data}" do
|
36
|
+
|
37
|
+
before(:all) do
|
38
|
+
User.delete_all
|
39
|
+
User.create!(:login => 'test 1', :created_at => Time.now, :profile_visits => 2)
|
40
|
+
User.create!(:login => 'test 2', :created_at => Time.now - 1.send(grouping), :profile_visits => 1)
|
41
|
+
User.create!(:login => 'test 3', :created_at => Time.now - 3.send(grouping), :profile_visits => 2)
|
42
|
+
User.create!(:login => 'test 4', :created_at => Time.now - 3.send(grouping), :profile_visits => 3)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'the returned result' do
|
46
|
+
|
47
|
+
before do
|
48
|
+
@grouping = Saulabs::Reportable::Grouping.new(grouping)
|
49
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations,
|
50
|
+
:grouping => grouping,
|
51
|
+
:limit => 10,
|
52
|
+
:live_data => live_data
|
53
|
+
)
|
54
|
+
@result = @report.run
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be an array starting reporting period (Time.now - limit.#{grouping.to_s})" do
|
58
|
+
@result.first[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping, Time.now - 10.send(grouping)).date_time
|
59
|
+
end
|
60
|
+
|
61
|
+
if live_data
|
62
|
+
it "should be data ending with the current reporting period" do
|
63
|
+
@result.last[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping).date_time
|
64
|
+
end
|
65
|
+
else
|
66
|
+
it "should be data ending with the reporting period before the current" do
|
67
|
+
@result.last[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping).previous.date_time
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should return correct data for aggregation :count' do
|
74
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
|
75
|
+
:aggregation => :count,
|
76
|
+
:grouping => grouping,
|
77
|
+
:limit => 10,
|
78
|
+
:live_data => live_data
|
79
|
+
)
|
80
|
+
result = @report.run
|
81
|
+
|
82
|
+
result[10][1].should == 4.0 if live_data
|
83
|
+
result[9][1].should == 3.0
|
84
|
+
result[8][1].should == 2.0
|
85
|
+
result[7][1].should == 2.0
|
86
|
+
result[6][1].should == 0.0
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should return correct data for aggregation :sum' do
|
90
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
|
91
|
+
:aggregation => :sum,
|
92
|
+
:grouping => grouping,
|
93
|
+
:value_column => :profile_visits,
|
94
|
+
:limit => 10,
|
95
|
+
:live_data => live_data
|
96
|
+
)
|
97
|
+
result = @report.run()
|
98
|
+
|
99
|
+
result[10][1].should == 8.0 if live_data
|
100
|
+
result[9][1].should == 6.0
|
101
|
+
result[8][1].should == 5.0
|
102
|
+
result[7][1].should == 5.0
|
103
|
+
result[6][1].should == 0.0
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should return correct data for aggregation :count when custom conditions are specified' do
|
107
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
|
108
|
+
:aggregation => :count,
|
109
|
+
:grouping => grouping,
|
110
|
+
:limit => 10,
|
111
|
+
:live_data => live_data
|
112
|
+
)
|
113
|
+
result = @report.run(:conditions => ['login IN (?)', ['test 1', 'test 2', 'test 4']])
|
114
|
+
|
115
|
+
result[10][1].should == 3.0 if live_data
|
116
|
+
result[9][1].should == 2.0
|
117
|
+
result[8][1].should == 1.0
|
118
|
+
result[7][1].should == 1.0
|
119
|
+
result[6][1].should == 0.0
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should return correct data for aggregation :sum when custom conditions are specified' do
|
123
|
+
@report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
|
124
|
+
:aggregation => :sum,
|
125
|
+
:grouping => grouping,
|
126
|
+
:value_column => :profile_visits,
|
127
|
+
:limit => 10,
|
128
|
+
:live_data => live_data
|
129
|
+
)
|
130
|
+
result = @report.run(:conditions => ['login IN (?)', ['test 1', 'test 2', 'test 4']])
|
131
|
+
|
132
|
+
result[10][1].should == 6.0 if live_data
|
133
|
+
result[9][1].should == 4.0
|
134
|
+
result[8][1].should == 3.0
|
135
|
+
result[7][1].should == 3.0
|
136
|
+
result[6][1].should == 0.0
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
after(:all) do
|
142
|
+
User.destroy_all
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
after(:each) do
|
152
|
+
Saulabs::Reportable::ReportCache.destroy_all
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
describe '#cumulate' do
|
158
|
+
|
159
|
+
it 'should correctly cumulate the given data' do
|
160
|
+
first = (Time.now - 1.week).to_s
|
161
|
+
second = Time.now.to_s
|
162
|
+
data = [[first, 1], [second, 2]]
|
163
|
+
|
164
|
+
@report.send(:cumulate, data, @report.send(:options_for_run, {})).should == [[first, 1.0], [second, 3.0]]
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|