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