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
data/HISTORY.md
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008-2010 Marco Otte-Witte, Martin Kavalar
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
Reportable
|
2
|
+
==========
|
3
|
+
|
4
|
+
Reportable allows for the easy creation of reports based on `ActiveRecord` models.
|
5
|
+
|
6
|
+
|
7
|
+
Usage
|
8
|
+
-----
|
9
|
+
|
10
|
+
Usage is pretty easy. To declare a report on a model, simply define that the model provides a report:
|
11
|
+
|
12
|
+
class User < ActiveRecord::Base
|
13
|
+
|
14
|
+
reportable :registrations, :aggregation => :count
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
The `reportable` method takes a bunch more options which are described in the [API docs](http://rdoc.info/projects/saulabs/reportable). For example you could generate a report on
|
19
|
+
the number of updated users records per second or the number of registrations of users that have a last name that starts with `'A'` per month:
|
20
|
+
|
21
|
+
class User < ActiveRecord::Base
|
22
|
+
|
23
|
+
reportable :last_name_starting_with_a_registrations, :aggregation => :count, :grouping => :month, :conditions => ["last_name LIKE 'A%'"]
|
24
|
+
|
25
|
+
reportable :updated_per_second, :aggregation => :count, :grouping => :second, :date_column => :updated_at
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
For every declared report a method is generated on the model that returns the date:
|
30
|
+
|
31
|
+
User.registrations_report
|
32
|
+
|
33
|
+
User.last_name_starting_with_a_registrations_report
|
34
|
+
|
35
|
+
User.updated_per_second_report
|
36
|
+
|
37
|
+
|
38
|
+
Working with the data
|
39
|
+
---------------------
|
40
|
+
|
41
|
+
The data is returned as an `Array` of `Array`s of `DateTime`s and `Float`s, e.g.:
|
42
|
+
|
43
|
+
[
|
44
|
+
[DateTime.now, 1.0],
|
45
|
+
[DateTime.now - 1.day, 2.0],
|
46
|
+
[DateTime.now - 2.days, 3.0]
|
47
|
+
]
|
48
|
+
|
49
|
+
Reportable provides a helper method to generate a sparkline image from this data that you can use in your views:
|
50
|
+
|
51
|
+
<%= sparkline_tag(User.registrations_report) %>
|
52
|
+
|
53
|
+
|
54
|
+
Installation
|
55
|
+
------------
|
56
|
+
|
57
|
+
To install Reportable, simply run
|
58
|
+
|
59
|
+
[sudo] gem install reportable
|
60
|
+
|
61
|
+
and add it to your application's dependencies in your `environment.rb`:
|
62
|
+
|
63
|
+
config.gem 'reportable', :lib => 'saulabs/reportable'
|
64
|
+
|
65
|
+
When you installed the gem, you have to generate the migration that creates Reportable's cache table:
|
66
|
+
|
67
|
+
./script/generate reportable_migration create_reportable_cache
|
68
|
+
|
69
|
+
and migrate:
|
70
|
+
|
71
|
+
rake db:migrate
|
72
|
+
|
73
|
+
|
74
|
+
Plans
|
75
|
+
-----
|
76
|
+
|
77
|
+
* add support for Oracle and MSSQL
|
78
|
+
* add support for DataMapper
|
79
|
+
* add more options to generate graphs from the data
|
80
|
+
* add the option to generate textual reports on the command line
|
81
|
+
|
82
|
+
|
83
|
+
Authors
|
84
|
+
-------
|
85
|
+
|
86
|
+
© 2008-2010 Marco Otte-Witte (<http://simplabs.com>), Martin Kavalar (<http://www.sauspiel.de>)
|
87
|
+
|
88
|
+
Released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
Bundler.setup
|
6
|
+
Bundler.require
|
7
|
+
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
require 'simplabs/excellent/rake'
|
10
|
+
|
11
|
+
desc 'Default: run specs.'
|
12
|
+
task :default => :spec
|
13
|
+
|
14
|
+
desc 'Run the specs'
|
15
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
16
|
+
t.rcov_opts << '--exclude "gems/*,spec/*,init.rb"'
|
17
|
+
t.rcov = true
|
18
|
+
t.rcov_dir = 'doc/coverage'
|
19
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
20
|
+
end
|
21
|
+
|
22
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
23
|
+
t.files = ['lib/**/*.rb', '-', 'HISTORY.md']
|
24
|
+
t.options = ['--no-private', '--title', 'Reportable Documentation']
|
25
|
+
end
|
26
|
+
|
27
|
+
Simplabs::Excellent::Rake::ExcellentTask.new(:excellent) do |t|
|
28
|
+
t.paths = %w(lib)
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class <%= class_name %> < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
create_table :reportable_cache, :force => true do |t|
|
5
|
+
t.string :model_name, :null => false
|
6
|
+
t.string :report_name, :null => false
|
7
|
+
t.string :grouping, :null => false
|
8
|
+
t.string :aggregation, :null => false
|
9
|
+
t.string :condition, :null => false
|
10
|
+
t.float :value, :null => false, :default => 0
|
11
|
+
t.datetime :reporting_period, :null => false
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :reportable_cache, [
|
17
|
+
:model_name,
|
18
|
+
:report_name,
|
19
|
+
:grouping,
|
20
|
+
:aggregation,
|
21
|
+
:condition
|
22
|
+
], :name => :name_model_grouping_agregation
|
23
|
+
add_index :reportable_cache, [
|
24
|
+
:model_name,
|
25
|
+
:report_name,
|
26
|
+
:grouping,
|
27
|
+
:aggregation,
|
28
|
+
:condition,
|
29
|
+
:reporting_period
|
30
|
+
], :unique => true, :name => :name_model_grouping_aggregation_period
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.down
|
34
|
+
remove_index :reportable_cache, :name => :name_model_grouping_agregation
|
35
|
+
remove_index :reportable_cache, :name => :name_model_grouping_aggregation_period
|
36
|
+
|
37
|
+
drop_table :reportable_cache
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# Extends the {Saulabs::Reportable::ClassMethods#reportable} method into +base+.
|
6
|
+
#
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# Generates a report on a model. That report can then be executed via the new method +<name>_report+ (see documentation of {Saulabs::Reportable::Report#run}).
|
14
|
+
#
|
15
|
+
# @param [String] name
|
16
|
+
# the name of the report, also defines the name of the generated report method (+<name>_report+)
|
17
|
+
# @param [Hash] options
|
18
|
+
# the options to generate the reports with
|
19
|
+
#
|
20
|
+
# @option options [Symbol] :date_column (created_at)
|
21
|
+
# the name of the date column over that the records are aggregated
|
22
|
+
# @option options [String, Symbol] :value_column (:id)
|
23
|
+
# the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
|
24
|
+
# @option options [Symbol] :aggregation (:count)
|
25
|
+
# the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
|
26
|
+
# @option options [Symbol] :grouping (:day)
|
27
|
+
# the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
|
28
|
+
# @option options [Fixnum] :limit (100)
|
29
|
+
# the number of reporting periods to get (see +:grouping+)
|
30
|
+
# @option options [Hash] :conditions ({})
|
31
|
+
# conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
|
32
|
+
# @option options [Boolean] :live_data (false)
|
33
|
+
# 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>
|
34
|
+
# @option options [DateTime, Boolean] :end_date (false)
|
35
|
+
# when specified, the report will only include data for the +:limit+ reporting periods until this date.
|
36
|
+
#
|
37
|
+
# @example Declaring reports on a model
|
38
|
+
#
|
39
|
+
# class User < ActiveRecord::Base
|
40
|
+
# reportable :registrations, :aggregation => :count
|
41
|
+
# reportable :activations, :aggregation => :count, :date_column => :activated_at
|
42
|
+
# reportable :total_users, :cumulate => true
|
43
|
+
# reportable :rake, :aggregation => :sum, :value_column => :profile_visits
|
44
|
+
# end
|
45
|
+
def reportable(name, options = {})
|
46
|
+
(class << self; self; end).instance_eval do
|
47
|
+
define_method "#{name.to_s}_report".to_sym do |*args|
|
48
|
+
if options.delete(:cumulate)
|
49
|
+
report = Saulabs::Reportable::CumulatedReport.new(self, name, options)
|
50
|
+
else
|
51
|
+
report = Saulabs::Reportable::Report.new(self, name, options)
|
52
|
+
end
|
53
|
+
raise ArgumentError.new unless args.length == 0 || (args.length == 1 && args[0].is_a?(Hash))
|
54
|
+
report.run(args.length == 0 ? {} : args[0])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# A special report class that cumulates all data (see {Saulabs::Reportable::Report})
|
6
|
+
#
|
7
|
+
# @example Cumulated reports as opposed to regular reports
|
8
|
+
#
|
9
|
+
# [[<DateTime today>, 1], [<DateTime yesterday>, 2], etc.]
|
10
|
+
# [[<DateTime today>, 3], [<DateTime yesterday>, 2], etc.]
|
11
|
+
#
|
12
|
+
class CumulatedReport < Report
|
13
|
+
|
14
|
+
# Runs the report (see {Saulabs::Reportable::Report#run})
|
15
|
+
#
|
16
|
+
def run(options = {})
|
17
|
+
cumulate(super, options_for_run(options))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def cumulate(data, options)
|
23
|
+
first_reporting_period = ReportingPeriod.first(options[:grouping], options[:limit], options[:end_date])
|
24
|
+
acc = initial_cumulative_value(first_reporting_period.date_time, options)
|
25
|
+
result = []
|
26
|
+
data.each do |element|
|
27
|
+
acc += element[1].to_f
|
28
|
+
result << [element[0], acc]
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def initial_cumulative_value(date, options)
|
34
|
+
conditions = setup_conditions(nil, date, options[:conditions])
|
35
|
+
@klass.send(@aggregation, @value_column, :conditions => conditions)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# The grouping specifies which records are grouped into one {Saulabs::Reportable::ReportingPeriod}.
|
6
|
+
#
|
7
|
+
class Grouping
|
8
|
+
|
9
|
+
# Initializes a new grouping.
|
10
|
+
#
|
11
|
+
# @param [Symbol] identifier
|
12
|
+
# the identifier of the grouping (one of +:hour+, +:day+, +:week+ or +:month+)
|
13
|
+
#
|
14
|
+
def initialize(identifier)
|
15
|
+
raise ArgumentError.new("Invalid grouping #{identifier}") unless [:hour, :day, :week, :month].include?(identifier)
|
16
|
+
@identifier = identifier
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets the identifier of the grouping.
|
20
|
+
#
|
21
|
+
# @return [Symbol]
|
22
|
+
# the identifier of the grouping.
|
23
|
+
#
|
24
|
+
def identifier
|
25
|
+
@identifier
|
26
|
+
end
|
27
|
+
|
28
|
+
# Gets an array of date parts from a DB string.
|
29
|
+
#
|
30
|
+
# @param [String] db_string
|
31
|
+
# the DB string to get the date parts from
|
32
|
+
#
|
33
|
+
# @return [Array<Fixnum>]
|
34
|
+
# array of numbers that represent the values of the date
|
35
|
+
#
|
36
|
+
def date_parts_from_db_string(db_string)
|
37
|
+
case ActiveRecord::Base.connection.adapter_name
|
38
|
+
when /mysql/i
|
39
|
+
from_mysql_db_string(db_string)
|
40
|
+
when /sqlite/i
|
41
|
+
from_sqlite_db_string(db_string)
|
42
|
+
when /postgres/i
|
43
|
+
from_postgresql_db_string(db_string)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Converts the grouping into a DB specific string that can be used to group records.
|
48
|
+
#
|
49
|
+
# @param [String] date_column
|
50
|
+
# the name of the DB column that holds the date
|
51
|
+
#
|
52
|
+
def to_sql(date_column)
|
53
|
+
case ActiveRecord::Base.connection.adapter_name
|
54
|
+
when /mysql/i
|
55
|
+
mysql_format(date_column)
|
56
|
+
when /sqlite/i
|
57
|
+
sqlite_format(date_column)
|
58
|
+
when /postgres/i
|
59
|
+
postgresql_format(date_column)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def from_mysql_db_string(db_string)
|
66
|
+
if @identifier == :week
|
67
|
+
parts = [db_string[0..3], db_string[4..5]].map(&:to_i)
|
68
|
+
else
|
69
|
+
db_string.split('/').map(&:to_i)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def from_sqlite_db_string(db_string)
|
74
|
+
if @identifier == :week
|
75
|
+
parts = db_string.split('-').map(&:to_i)
|
76
|
+
date = Date.new(parts[0], parts[1], parts[2])
|
77
|
+
return [date.cwyear, date.cweek]
|
78
|
+
end
|
79
|
+
db_string.split('/').map(&:to_i)
|
80
|
+
end
|
81
|
+
|
82
|
+
def from_postgresql_db_string(db_string)
|
83
|
+
case @identifier
|
84
|
+
when :hour
|
85
|
+
return (db_string[0..9].split('-') + [db_string[11..12]]).map(&:to_i)
|
86
|
+
when :day
|
87
|
+
return db_string[0..9].split('-').map(&:to_i)
|
88
|
+
when :week
|
89
|
+
parts = db_string[0..9].split('-').map(&:to_i)
|
90
|
+
date = Date.new(parts[0], parts[1], parts[2])
|
91
|
+
return [date.cwyear, date.cweek]
|
92
|
+
when :month
|
93
|
+
return db_string[0..6].split('-')[0..1].map(&:to_i)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def mysql_format(date_column)
|
98
|
+
case @identifier
|
99
|
+
when :hour
|
100
|
+
"DATE_FORMAT(#{date_column}, '%Y/%m/%d/%H')"
|
101
|
+
when :day
|
102
|
+
"DATE_FORMAT(#{date_column}, '%Y/%m/%d')"
|
103
|
+
when :week
|
104
|
+
"YEARWEEK(#{date_column}, 3)"
|
105
|
+
when :month
|
106
|
+
"DATE_FORMAT(#{date_column}, '%Y/%m')"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def sqlite_format(date_column)
|
111
|
+
case @identifier
|
112
|
+
when :hour
|
113
|
+
"strftime('%Y/%m/%d/%H', #{date_column})"
|
114
|
+
when :day
|
115
|
+
"strftime('%Y/%m/%d', #{date_column})"
|
116
|
+
when :week
|
117
|
+
"date(#{date_column}, 'weekday 0')"
|
118
|
+
when :month
|
119
|
+
"strftime('%Y/%m', #{date_column})"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def postgresql_format(date_column)
|
124
|
+
case @identifier
|
125
|
+
when :hour
|
126
|
+
"date_trunc('hour', #{date_column})"
|
127
|
+
when :day
|
128
|
+
"date_trunc('day', #{date_column})"
|
129
|
+
when :week
|
130
|
+
"date_trunc('week', #{date_column})"
|
131
|
+
when :month
|
132
|
+
"date_trunc('month', #{date_column})"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Saulabs
|
2
|
+
|
3
|
+
module Reportable
|
4
|
+
|
5
|
+
# The Report class that does all the data retrieval and calculations.
|
6
|
+
#
|
7
|
+
class Report
|
8
|
+
|
9
|
+
# the model the report works on (This is the class you invoke {Saulabs::Reportable::ClassMethods#reportable} on)
|
10
|
+
#
|
11
|
+
attr_reader :klass
|
12
|
+
|
13
|
+
# the name of the report (as in {Saulabs::Reportable::ClassMethods#reportable})
|
14
|
+
#
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# the name of the date column over that the records are aggregated
|
18
|
+
#
|
19
|
+
attr_reader :date_column
|
20
|
+
|
21
|
+
# the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
|
22
|
+
#
|
23
|
+
attr_reader :value_column
|
24
|
+
|
25
|
+
# the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
|
26
|
+
#
|
27
|
+
attr_reader :aggregation
|
28
|
+
|
29
|
+
# options for the report
|
30
|
+
#
|
31
|
+
attr_reader :options
|
32
|
+
|
33
|
+
# Initializes a new {Saulabs::Reportable::Report}
|
34
|
+
#
|
35
|
+
# @param [Class] klass
|
36
|
+
# the model the report works on (This is the class you invoke {Saulabs::Reportable::ClassMethods#reportable} on)
|
37
|
+
# @param [String] name
|
38
|
+
# the name of the report (as in {Saulabs::Reportable::ClassMethods#reportable})
|
39
|
+
# @param [Hash] options
|
40
|
+
# options for the report creation
|
41
|
+
#
|
42
|
+
# @option options [Symbol] :date_column (created_at)
|
43
|
+
# the name of the date column over that the records are aggregated
|
44
|
+
# @option options [String, Symbol] :value_column (:id)
|
45
|
+
# the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
|
46
|
+
# @option options [Symbol] :aggregation (:count)
|
47
|
+
# the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
|
48
|
+
# @option options [Symbol] :grouping (:day)
|
49
|
+
# the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
|
50
|
+
# @option options [Fixnum] :limit (100)
|
51
|
+
# the number of reporting periods to get (see +:grouping+)
|
52
|
+
# @option options [Hash] :conditions ({})
|
53
|
+
# conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
|
54
|
+
# @option options [Boolean] :live_data (false)
|
55
|
+
# 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>
|
56
|
+
# @option options [DateTime, Boolean] :end_date (false)
|
57
|
+
# when specified, the report will only include data for the +:limit+ reporting periods until this date.
|
58
|
+
#
|
59
|
+
def initialize(klass, name, options = {})
|
60
|
+
ensure_valid_options(options)
|
61
|
+
@klass = klass
|
62
|
+
@name = name
|
63
|
+
@date_column = (options[:date_column] || 'created_at').to_s
|
64
|
+
@aggregation = options[:aggregation] || :count
|
65
|
+
@value_column = (options[:value_column] || (@aggregation == :count ? 'id' : name)).to_s
|
66
|
+
@options = {
|
67
|
+
:limit => options[:limit] || 100,
|
68
|
+
:conditions => options[:conditions] || [],
|
69
|
+
:grouping => Grouping.new(options[:grouping] || :day),
|
70
|
+
:live_data => options[:live_data] || false,
|
71
|
+
:end_date => options[:end_date] || false
|
72
|
+
}
|
73
|
+
@options.merge!(options)
|
74
|
+
@options.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
# Runs the report and returns an array of array of DateTimes and Floats
|
78
|
+
#
|
79
|
+
# @param [Hash] options
|
80
|
+
# options to run the report with
|
81
|
+
#
|
82
|
+
# @option options [Symbol] :grouping (:day)
|
83
|
+
# the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
|
84
|
+
# @option options [Fixnum] :limit (100)
|
85
|
+
# the number of reporting periods to get (see +:grouping+)
|
86
|
+
# @option options [Hash] :conditions ({})
|
87
|
+
# conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
|
88
|
+
# @option options [Boolean] :live_data (false)
|
89
|
+
# 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>
|
90
|
+
# @option options [DateTime, Boolean] :end_date (false)
|
91
|
+
# when specified, the report will only include data for the +:limit+ reporting periods until this date.
|
92
|
+
#
|
93
|
+
# @return [Array<Array<DateTime, Float>>]
|
94
|
+
# the result of the report as pairs of {DateTime}s and {Float}s
|
95
|
+
#
|
96
|
+
def run(options = {})
|
97
|
+
options = options_for_run(options)
|
98
|
+
ReportCache.process(self, options) do |begin_at, end_at|
|
99
|
+
read_data(begin_at, end_at, options)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def options_for_run(options = {})
|
106
|
+
options = options.dup
|
107
|
+
ensure_valid_options(options, :run)
|
108
|
+
options.reverse_merge!(@options)
|
109
|
+
options[:grouping] = Grouping.new(options[:grouping]) unless options[:grouping].is_a?(Grouping)
|
110
|
+
return options
|
111
|
+
end
|
112
|
+
|
113
|
+
def read_data(begin_at, end_at, options)
|
114
|
+
conditions = setup_conditions(begin_at, end_at, options[:conditions])
|
115
|
+
@klass.send(@aggregation,
|
116
|
+
@value_column,
|
117
|
+
:conditions => conditions,
|
118
|
+
:group => options[:grouping].to_sql(@date_column),
|
119
|
+
:order => "#{options[:grouping].to_sql(@date_column)} ASC",
|
120
|
+
:limit => options[:limit]
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
def setup_conditions(begin_at, end_at, custom_conditions = [])
|
125
|
+
conditions = [@klass.send(:sanitize_sql_for_conditions, custom_conditions) || '']
|
126
|
+
conditions[0] += "#{(conditions[0].blank? ? '' : ' AND ')}#{ActiveRecord::Base.connection.quote_table_name(@klass.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(@date_column.to_s)} "
|
127
|
+
conditions[0] += if begin_at && end_at
|
128
|
+
'BETWEEN ? AND ?'
|
129
|
+
elsif begin_at
|
130
|
+
'>= ?'
|
131
|
+
elsif end_at
|
132
|
+
'<= ?'
|
133
|
+
else
|
134
|
+
raise ArgumentError.new('You must pass either begin_at, end_at or both to setup_conditions.')
|
135
|
+
end
|
136
|
+
conditions << begin_at if begin_at
|
137
|
+
conditions << end_at if end_at
|
138
|
+
conditions
|
139
|
+
end
|
140
|
+
|
141
|
+
def ensure_valid_options(options, context = :initialize)
|
142
|
+
case context
|
143
|
+
when :initialize
|
144
|
+
options.each_key do |k|
|
145
|
+
raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :date_column, :value_column, :conditions, :live_data, :end_date].include?(k)
|
146
|
+
end
|
147
|
+
raise ArgumentError.new("Invalid aggregation #{options[:aggregation]}!") if options[:aggregation] && ![:count, :sum, :maximum, :minimum, :average].include?(options[:aggregation])
|
148
|
+
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)
|
149
|
+
when :run
|
150
|
+
options.each_key do |k|
|
151
|
+
raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :grouping, :live_data, :end_date].include?(k)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
raise ArgumentError.new('Options :live_data and :end_date may not both be specified!') if options[:live_data] && options[:end_date]
|
155
|
+
raise ArgumentError.new("Invalid grouping #{options[:grouping]}!") if options[:grouping] && ![:hour, :day, :week, :month].include?(options[:grouping])
|
156
|
+
raise ArgumentError.new("Invalid conditions: #{options[:conditions].inspect}!") if options[:conditions] && !options[:conditions].is_a?(Array) && !options[:conditions].is_a?(Hash)
|
157
|
+
raise ArgumentError.new("Invalid end date: #{options[:end_date].inspect}; must be a DateTime!") if options[:end_date] && !options[:end_date].is_a?(DateTime) && !options[:end_date].is_a?(Time)
|
158
|
+
raise ArgumentError.new('End date may not be in the future!') if options[:end_date] && options[:end_date] > DateTime.now
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|