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
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
|