report_cat 0.2.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +147 -0
- data/Rakefile +24 -0
- data/app/assets/javascripts/report_cat/application.js +13 -0
- data/app/assets/stylesheets/report_cat/application.css +13 -0
- data/app/controllers/report_cat/reports_controller.rb +43 -0
- data/app/helpers/report_cat/reports_helper.rb +132 -0
- data/app/models/report_cat/date_range.rb +94 -0
- data/app/views/report_cat/reports/_google_charts.html.erb +61 -0
- data/app/views/report_cat/reports/index.html.erb +2 -0
- data/app/views/report_cat/reports/show.html.erb +15 -0
- data/config/locales/en.yml +26 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20130918075200_create_date_ranges.rb +13 -0
- data/lib/report_cat/config.rb +30 -0
- data/lib/report_cat/core/chart.rb +50 -0
- data/lib/report_cat/core/column.rb +70 -0
- data/lib/report_cat/core/param.rb +35 -0
- data/lib/report_cat/core/report.rb +127 -0
- data/lib/report_cat/engine.rb +11 -0
- data/lib/report_cat/matchers/have_chart.rb +58 -0
- data/lib/report_cat/matchers/have_column.rb +45 -0
- data/lib/report_cat/matchers/have_param.rb +52 -0
- data/lib/report_cat/reports/cohort_report.rb +113 -0
- data/lib/report_cat/reports/date_range_report.rb +66 -0
- data/lib/report_cat/version.rb +3 -0
- data/lib/report_cat.rb +39 -0
- data/lib/tasks/report_cat.rake +4 -0
- data/spec/controllers/report_cat/reports_controller_spec.rb +100 -0
- data/spec/coverage_spec.rb +18 -0
- data/spec/data/helpers/report_charts.html +1 -0
- data/spec/data/helpers/report_charts.html.tmp +1 -0
- data/spec/data/helpers/report_form.html +63 -0
- data/spec/data/helpers/report_form.html.tmp +63 -0
- data/spec/data/helpers/report_form_param.html +1 -0
- data/spec/data/helpers/report_form_param.html.tmp +1 -0
- data/spec/data/helpers/report_list.html +1 -0
- data/spec/data/helpers/report_list.html.tmp +1 -0
- data/spec/data/helpers/report_param_checkbox.html +1 -0
- data/spec/data/helpers/report_param_checkbox.html.tmp +1 -0
- data/spec/data/helpers/report_param_date.html +60 -0
- data/spec/data/helpers/report_param_date.html.tmp +60 -0
- data/spec/data/helpers/report_param_hidden.html +1 -0
- data/spec/data/helpers/report_param_hidden.html.tmp +1 -0
- data/spec/data/helpers/report_param_select.html +3 -0
- data/spec/data/helpers/report_param_select.html.tmp +3 -0
- data/spec/data/helpers/report_param_text_field.html +1 -0
- data/spec/data/helpers/report_param_text_field.html.tmp +1 -0
- data/spec/data/helpers/report_table.html +1 -0
- data/spec/data/helpers/report_table.html.tmp +1 -0
- data/spec/data/helpers/report_table_hidden.html +1 -0
- data/spec/data/helpers/report_table_hidden.html.tmp +1 -0
- data/spec/data/lib/chart_columns.json +1 -0
- data/spec/data/lib/chart_columns.json.tmp +1 -0
- data/spec/data/lib/chart_data.json +1 -0
- data/spec/data/lib/chart_data.json.tmp +1 -0
- data/spec/data/lib/date_range_report_where.sql +6 -0
- data/spec/data/lib/date_range_report_where.sql.tmp +6 -0
- data/spec/data/lib/report.csv +3 -0
- data/spec/data/lib/report.csv.tmp +3 -0
- data/spec/data/lib/report.sql +1 -0
- data/spec/data/lib/report.sql.tmp +1 -0
- data/spec/data/models/sql_intersect.sql +5 -0
- data/spec/data/models/sql_intersect.sql.tmp +5 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +21 -0
- data/spec/dummy/app/controllers/root_controller.rb +20 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/user.rb +13 -0
- data/spec/dummy/app/models/visit.rb +16 -0
- data/spec/dummy/app/reports/retention_cohort_report.rb +19 -0
- data/spec/dummy/app/reports/retention_report.rb +30 -0
- data/spec/dummy/app/reports/user_report.rb +23 -0
- data/spec/dummy/app/views/layouts/admin.html.erb +19 -0
- data/spec/dummy/app/views/layouts/application.html.erb +19 -0
- data/spec/dummy/app/views/root/index.html.erb +8 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +30 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +31 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/report_cat.rb +15 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +38 -0
- data/spec/dummy/config/routes.rb +13 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +26 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +61 -0
- data/spec/dummy/log/test.log +26478 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/helpers/report_cat/reports_helper_spec.rb +224 -0
- data/spec/lib/report_cat/config_spec.rb +96 -0
- data/spec/lib/report_cat/core/chart_spec.rb +67 -0
- data/spec/lib/report_cat/core/column_spec.rb +156 -0
- data/spec/lib/report_cat/core/param_spec.rb +95 -0
- data/spec/lib/report_cat/core/report_spec.rb +342 -0
- data/spec/lib/report_cat/engine_spec.rb +9 -0
- data/spec/lib/report_cat/matchers/have_chart_spec.rb +36 -0
- data/spec/lib/report_cat/matchers/have_column_spec.rb +30 -0
- data/spec/lib/report_cat/matchers/have_param_spec.rb +33 -0
- data/spec/lib/report_cat/reports/cohort_report_spec.rb +215 -0
- data/spec/lib/report_cat/reports/date_range_report_spec.rb +125 -0
- data/spec/lib/report_cat/version_spec.rb +11 -0
- data/spec/lib/report_cat_spec.rb +62 -0
- data/spec/lib/tasks/report_cat.rake_spec.rb +13 -0
- data/spec/models/report_cat/date_range_spec.rb +144 -0
- data/spec/rails_helper.rb +49 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/setup_reports.rb +28 -0
- data/spec/views/report_cat/reports/index.html.erb_spec.rb +16 -0
- data/spec/views/report_cat/reports/show.html.erb_spec.rb +19 -0
- metadata +489 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ReportCat
|
|
2
|
+
module Core
|
|
3
|
+
class Chart
|
|
4
|
+
|
|
5
|
+
attr_reader :name, :type, :label, :values, :options
|
|
6
|
+
|
|
7
|
+
def initialize( attributes = {} )
|
|
8
|
+
@name = attributes[ :name ]
|
|
9
|
+
@type = attributes[ :type ]
|
|
10
|
+
@label = attributes[ :label ]
|
|
11
|
+
@values = attributes[ :values ] || []
|
|
12
|
+
@options = attributes[ :options ] || {}
|
|
13
|
+
|
|
14
|
+
@values = [ @values ] unless @values.is_a?( Array )
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns columns as JSON for the Google Visualization API
|
|
18
|
+
|
|
19
|
+
def columns( report )
|
|
20
|
+
columns = []
|
|
21
|
+
columns << [ 'string', @label ]
|
|
22
|
+
@values.each { |name| columns << [ 'number', name ] }
|
|
23
|
+
|
|
24
|
+
return columns.to_json
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns rows as JSON for the Google Visualization API
|
|
28
|
+
|
|
29
|
+
def data( report )
|
|
30
|
+
table = []
|
|
31
|
+
|
|
32
|
+
label_index = report.column_index( @label )
|
|
33
|
+
raise "Bad label index: #{@label}" unless label_index
|
|
34
|
+
|
|
35
|
+
value_indexes = @values.map { |name| report.column_index( name ) }
|
|
36
|
+
|
|
37
|
+
report.rows.each do |row|
|
|
38
|
+
data = [ row[ label_index ].to_s ]
|
|
39
|
+
value_indexes.each do |value_index|
|
|
40
|
+
data << ( value_index ? row[ value_index ] : nil )
|
|
41
|
+
end
|
|
42
|
+
table << data
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return table.to_json
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module ReportCat
|
|
2
|
+
module Core
|
|
3
|
+
class Column
|
|
4
|
+
|
|
5
|
+
attr_reader :name, :type, :options
|
|
6
|
+
|
|
7
|
+
def initialize( attributes = {} )
|
|
8
|
+
@name = attributes[ :name ]
|
|
9
|
+
@type = attributes[ :type ]
|
|
10
|
+
@options = attributes[ :options ] || {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format( value )
|
|
14
|
+
return nil if value.nil?
|
|
15
|
+
|
|
16
|
+
case @type
|
|
17
|
+
when :float then return ("%.2f" % value).to_f
|
|
18
|
+
when :integer then return value.to_i
|
|
19
|
+
when :moving_average then return ("%.2f" % value).to_f
|
|
20
|
+
when :ratio then return ("%.2f" % value).to_f
|
|
21
|
+
else return value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def post_process( report )
|
|
26
|
+
case @type
|
|
27
|
+
when :moving_average then post_process_moving_average( report )
|
|
28
|
+
when :ratio then post_process_ratio( report )
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def post_process_moving_average( report )
|
|
33
|
+
i_moving_average = report.column_index( name )
|
|
34
|
+
i_target = report.column_index( @options[ :target ] )
|
|
35
|
+
|
|
36
|
+
interval = @options[ :interval ]
|
|
37
|
+
interval_max = interval - 1
|
|
38
|
+
n_rows = report.rows.length - 1
|
|
39
|
+
|
|
40
|
+
(interval_max..n_rows).each do |row|
|
|
41
|
+
sum = 0.0
|
|
42
|
+
(0..interval_max).each { |i| sum += report.rows[ row - i ][ i_target ] }
|
|
43
|
+
value = sum / interval
|
|
44
|
+
report.rows[ row ][ i_moving_average ] = format( value )
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def post_process_ratio( report )
|
|
49
|
+
i_ratio = report.column_index( name )
|
|
50
|
+
i_numerator = report.column_index( @options[ :numerator ] )
|
|
51
|
+
i_denominator = report.column_index( @options[ :denominator ] )
|
|
52
|
+
|
|
53
|
+
report.rows.each do |row|
|
|
54
|
+
numerator = row[ i_numerator ].to_f
|
|
55
|
+
denominator = row[ i_denominator ].to_f
|
|
56
|
+
value = ( denominator == 0.0 ) ? 0.0 : ( numerator / denominator )
|
|
57
|
+
row[ i_ratio ] = format( value )
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_sql
|
|
62
|
+
sql = @options[ :sql ]
|
|
63
|
+
return "#{sql} as #{name}" if sql
|
|
64
|
+
return "0 as #{name}" if @type == :ratio || @type == :moving_average
|
|
65
|
+
return @name
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module ReportCat
|
|
2
|
+
module Core
|
|
3
|
+
class Param
|
|
4
|
+
|
|
5
|
+
attr_reader :name, :type, :value, :options
|
|
6
|
+
|
|
7
|
+
def initialize( attributes = {} )
|
|
8
|
+
@name = attributes[ :name ]
|
|
9
|
+
@type = attributes[ :type ]
|
|
10
|
+
@value = attributes[ :value ]
|
|
11
|
+
@options = attributes[ :options ] || {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def value=( value )
|
|
15
|
+
@value = case @type
|
|
16
|
+
when :check_box then ( value == '1' || value == true || value == 'true' )
|
|
17
|
+
when :date
|
|
18
|
+
if value.kind_of?( Hash )
|
|
19
|
+
Date.new( value[:year].to_i, value[:month].to_i, value[:day].to_i )
|
|
20
|
+
elsif value.kind_of?( String )
|
|
21
|
+
Date.parse( value )
|
|
22
|
+
else
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
else value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hide
|
|
30
|
+
@options[ :hidden ] = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# This is the most basic report, a simple wrapped around an SQL generator
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
|
|
5
|
+
module ReportCat
|
|
6
|
+
module Core
|
|
7
|
+
class Report
|
|
8
|
+
extend ActiveSupport::DescendantsTracker
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :params, :columns, :rows, :charts
|
|
11
|
+
attr_reader :from, :joins, :where, :group_by, :order_by, :limit
|
|
12
|
+
attr_accessor :back
|
|
13
|
+
attr_reader :abstract
|
|
14
|
+
|
|
15
|
+
def initialize( attributes = {} )
|
|
16
|
+
@name = attributes[ :name ]
|
|
17
|
+
@from = accept_array( attributes[ :from ], ',' )
|
|
18
|
+
@joins = accept_array( attributes[ :joins ], ' ' )
|
|
19
|
+
@where = accept_array( attributes[ :where ], ' and ' )
|
|
20
|
+
@group_by = accept_array( attributes[ :group_by ], ',' )
|
|
21
|
+
@order_by = accept_array( attributes[ :order_by ], ',' )
|
|
22
|
+
@limit = attributes[ :limit ]
|
|
23
|
+
|
|
24
|
+
@back = attributes[ :back ]
|
|
25
|
+
|
|
26
|
+
@params = []
|
|
27
|
+
@columns = []
|
|
28
|
+
@rows = []
|
|
29
|
+
@charts = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def accept_array( array, separator )
|
|
33
|
+
return array unless array.is_a?( Array )
|
|
34
|
+
return array.join( separator )
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_chart( name, type, label, values, options = {} )
|
|
38
|
+
chart = Chart.new(
|
|
39
|
+
:name => name,
|
|
40
|
+
:type => type,
|
|
41
|
+
:label => label,
|
|
42
|
+
:values => values,
|
|
43
|
+
:options => options )
|
|
44
|
+
|
|
45
|
+
charts << chart
|
|
46
|
+
return chart
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add_column( name, type, options = {} )
|
|
50
|
+
columns << ( column = Column.new( :name => name, :type => type, :options => options ) )
|
|
51
|
+
return column
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_param( name, type, value = nil, options = {} )
|
|
55
|
+
params << ( param = Param.new( :name => name, :type => type, :value => value, :options => options ) )
|
|
56
|
+
return param
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def attributes
|
|
60
|
+
hash = { :id => name, :name => name }
|
|
61
|
+
hash[ :back ] = @back if @back
|
|
62
|
+
@params.each { |param| hash[ param.name ] = param.value }
|
|
63
|
+
return hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def column( name )
|
|
67
|
+
if index = columns.index { |c| c.name.to_s == name.to_s }
|
|
68
|
+
return columns[ index ]
|
|
69
|
+
end
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def column_index( name )
|
|
74
|
+
@columns.each_index { |index| return index if columns[ index ].name == name }
|
|
75
|
+
return nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def generate( options = {} )
|
|
79
|
+
@params.each { |param| param.value = options[ param.name ] if options[ param.name ] }
|
|
80
|
+
query
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def param( name )
|
|
84
|
+
if index = @params.index { |p| p.name.to_s == name.to_s }
|
|
85
|
+
return @params[ index ]
|
|
86
|
+
end
|
|
87
|
+
return nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_csv
|
|
91
|
+
CSV.generate( :force_quotes => true ) do |csv|
|
|
92
|
+
csv << @columns.map { |column| column.name }
|
|
93
|
+
@rows.each { |row| csv << row }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
def query
|
|
100
|
+
@rows = []
|
|
101
|
+
return unless results = ActiveRecord::Base.connection.execute( to_sql )
|
|
102
|
+
|
|
103
|
+
results.each do |row|
|
|
104
|
+
row = columns.map { |c| row[ c.name.to_s ] } if row.is_a?( Hash )
|
|
105
|
+
row.each_index { |i| row[ i ] = columns[ i ].format( row[ i ] ) }
|
|
106
|
+
@rows << row
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@columns.each { |c| c.post_process( self ) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_sql
|
|
113
|
+
select = @columns.map { |c| c.to_sql }.compact.join( ',' )
|
|
114
|
+
|
|
115
|
+
sql = "select #{select} from #{from}"
|
|
116
|
+
sql << " #{joins}" if joins
|
|
117
|
+
sql << " where #{where}" if where
|
|
118
|
+
sql << " group by #{group_by}" if group_by
|
|
119
|
+
sql << " order by #{order_by}" if order_by
|
|
120
|
+
sql << " limit #{limit}" if limit
|
|
121
|
+
|
|
122
|
+
return sql
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_chart do |name|
|
|
2
|
+
|
|
3
|
+
description do
|
|
4
|
+
"have a chart named #{name}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
chain :with_type do |type|
|
|
8
|
+
@type = type
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
chain :with_label do |label|
|
|
12
|
+
@label = label
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
chain :with_values do |values|
|
|
16
|
+
@values = values
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
chain :with_options do |options|
|
|
20
|
+
@options = options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
match do |report|
|
|
24
|
+
chart = report.charts.select{ |chart| chart.name == name }.first
|
|
25
|
+
|
|
26
|
+
@has_chart = !chart.nil?
|
|
27
|
+
@has_chart = ( chart.type == @type ) if @type && @has_chart
|
|
28
|
+
@has_chart = ( chart.label == @label ) if @label && @has_chart
|
|
29
|
+
@has_chart = ( chart.values == @values ) if @values && @has_chart
|
|
30
|
+
@has_chart = ( chart.options == @options ) if @options && @has_chart
|
|
31
|
+
|
|
32
|
+
@has_chart
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
failure_message do |report|
|
|
36
|
+
message = "expected that report would have a chart named #{name}"
|
|
37
|
+
|
|
38
|
+
chart = report.charts.select{ |chart| chart.name == name }.first
|
|
39
|
+
message << " with type #{@type} but got #{chart.type}" if chart && chart.type != @type
|
|
40
|
+
message << " with label #{@label} but got #{chart.label}" if chart && chart.label != @value
|
|
41
|
+
message << " with options #{@values} but got #{chart.values}" if chart && chart.values != @values
|
|
42
|
+
message << " with options #{@options} but got #{chart.options}" if chart && chart.options != @options
|
|
43
|
+
|
|
44
|
+
message
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
failure_message_when_negated do |report|
|
|
48
|
+
message = "expected that report would not chart a param named #{name}"
|
|
49
|
+
|
|
50
|
+
chart = report.charts.select{ |chart| chart.name == name }.first
|
|
51
|
+
message << " with type #{@type} but got #{chart.type}" if chart && chart.type != @type
|
|
52
|
+
message << " with label #{@label} but got #{chart.label}" if chart && chart.label != @value
|
|
53
|
+
message << " with options #{@values} but got #{chart.values}" if chart && chart.values != @values
|
|
54
|
+
message << " with options #{@options} but got #{chart.options}" if chart && chart.options != @options
|
|
55
|
+
|
|
56
|
+
message
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_column do |name|
|
|
2
|
+
|
|
3
|
+
description do
|
|
4
|
+
"have a column named #{name}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
chain :with_type do |type|
|
|
8
|
+
@type = type
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
chain :with_options do |options|
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
match do |report|
|
|
16
|
+
column = report.columns.select{ |column| column.name == name }.first
|
|
17
|
+
|
|
18
|
+
@has_column = !column.nil?
|
|
19
|
+
@has_column = ( column.type == @type ) if @type && @has_column
|
|
20
|
+
@has_column = ( column.options == @options ) if @options && @has_column
|
|
21
|
+
|
|
22
|
+
@has_column
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
failure_message do |report|
|
|
26
|
+
message = "expected that report would have a column named #{name}"
|
|
27
|
+
|
|
28
|
+
column = report.columns.select{ |column| column.name == name }.first
|
|
29
|
+
message << " with type #{@type} but got #{column.type}" if column && column.type != @type
|
|
30
|
+
message << " with options #{@options} but got #{column.options}" if column && column.options != @options
|
|
31
|
+
|
|
32
|
+
message
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
failure_message_when_negated do |report|
|
|
36
|
+
message = "expected that report would not have a column named #{name}"
|
|
37
|
+
|
|
38
|
+
column = report.columns.select{ |column| column.name == name }.first
|
|
39
|
+
message << " with type #{@type} but got #{column.type}" if column && column.type != @type
|
|
40
|
+
message << " with options #{@options} but got #{column.options}" if column && column.options != @options
|
|
41
|
+
|
|
42
|
+
message
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_param do |name|
|
|
2
|
+
|
|
3
|
+
description do
|
|
4
|
+
"have a param named #{name}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
chain :with_type do |type|
|
|
8
|
+
@type = type
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
chain :with_value do |value|
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
chain :with_options do |options|
|
|
16
|
+
@options = options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
match do |report|
|
|
20
|
+
param = report.params.select{ |param| param.name == name }.first
|
|
21
|
+
|
|
22
|
+
@has_param = !param.nil?
|
|
23
|
+
@has_param = ( param.type == @type ) if @type && @has_param
|
|
24
|
+
@has_param = ( param.value == @value ) if @value && @has_param
|
|
25
|
+
@has_param = ( param.options == @options ) if @options && @has_param
|
|
26
|
+
|
|
27
|
+
@has_param
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
failure_message do |report|
|
|
31
|
+
message = "expected that report would have a param named #{name}"
|
|
32
|
+
|
|
33
|
+
param = report.params.select{ |param| param.name == name }.first
|
|
34
|
+
message << " with type #{@type} but got #{param.type}" if param && param.type != @type
|
|
35
|
+
message << " with value #{@value} but got #{param.value}" if param && param.value != @value
|
|
36
|
+
message << " with options #{@options} but got #{param.options}" if param && param.options != @options
|
|
37
|
+
|
|
38
|
+
message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
failure_message_when_negated do |report|
|
|
42
|
+
message = "expected that report would not have a param named #{name}"
|
|
43
|
+
|
|
44
|
+
param = report.params.select{ |param| param.name == name }.first
|
|
45
|
+
message << " with type #{@type} but got #{param.type}" if param && param.type != @type
|
|
46
|
+
message << " with value #{@value} but got #{param.value}" if param && param.value != @value
|
|
47
|
+
message << " with options #{@options} but got #{param.options}" if param && param.options != @options
|
|
48
|
+
|
|
49
|
+
message
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module ReportCat
|
|
2
|
+
module Reports
|
|
3
|
+
include ReportCat::Core
|
|
4
|
+
|
|
5
|
+
class CohortReport < DateRangeReport
|
|
6
|
+
|
|
7
|
+
attr_reader :cohort
|
|
8
|
+
attr_reader :cohort_column
|
|
9
|
+
|
|
10
|
+
def initialize( attributes = {} )
|
|
11
|
+
defaults = { :name => :cohort_report }
|
|
12
|
+
super( defaults.merge( attributes ) )
|
|
13
|
+
|
|
14
|
+
@cohort_column = attributes[ :cohort_column ] || :total
|
|
15
|
+
|
|
16
|
+
add_column( :total, :integer )
|
|
17
|
+
|
|
18
|
+
# Assume any params the child report has
|
|
19
|
+
|
|
20
|
+
if @cohort = attributes[ :cohort ]
|
|
21
|
+
@cohort.params.each { |p| @params << p unless param( p.name ) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def query
|
|
26
|
+
@rows = []
|
|
27
|
+
|
|
28
|
+
period = param( :period ).value.to_sym
|
|
29
|
+
start_date = param( :start_date ).value
|
|
30
|
+
stop_date = param( :stop_date ).value
|
|
31
|
+
name = param( :period ).value.to_s.chop.chop
|
|
32
|
+
|
|
33
|
+
DateRange.generate( period, start_date, stop_date )
|
|
34
|
+
range = DateRange.range( period, start_date, stop_date )
|
|
35
|
+
|
|
36
|
+
range.each_index { |i| add_column( "#{name}_#{i+1}", :float ) }
|
|
37
|
+
range.each { |r| @rows << add_row( r, range ) }
|
|
38
|
+
|
|
39
|
+
columns = @columns[ 3, @columns.length - 3 ].map { |c| c.name }
|
|
40
|
+
add_chart( :cohort_line, :line, :start_date, columns )
|
|
41
|
+
|
|
42
|
+
add_link_column
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add_row( date_range, column_range )
|
|
46
|
+
return [] unless cohort
|
|
47
|
+
|
|
48
|
+
generate_cohort( date_range )
|
|
49
|
+
|
|
50
|
+
i_total = cohort.column_index( :total )
|
|
51
|
+
total = cohort.rows.empty? ? 0 : cohort.rows[ 0 ][ i_total ]
|
|
52
|
+
row = [ date_range.start_date, date_range.stop_date, total ]
|
|
53
|
+
|
|
54
|
+
column_range.each_index do |i|
|
|
55
|
+
if i >= cohort.rows.size
|
|
56
|
+
row << nil
|
|
57
|
+
else
|
|
58
|
+
row << process_cohort( cohort.rows[ i ] )
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return row
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def generate_cohort( date_range )
|
|
66
|
+
cohort.param( :period ).value = date_range.period.to_sym
|
|
67
|
+
cohort.param( :start_date ).value = date_range.start_date
|
|
68
|
+
cohort.param( :stop_date ).value = param( :stop_date ).value
|
|
69
|
+
cohort.generate
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process_cohort( row )
|
|
73
|
+
return raw_cohort( row )
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def raw_cohort( row )
|
|
77
|
+
i_total = cohort.column_index( cohort_column )
|
|
78
|
+
value = row[ i_total ].to_f
|
|
79
|
+
return ("%.2f" % value).to_f
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fractional_cohort( row )
|
|
83
|
+
i_total = cohort.column_index( cohort_column )
|
|
84
|
+
total = cohort.rows.empty? ? 0 : cohort.rows[ 0 ][ i_total ]
|
|
85
|
+
|
|
86
|
+
value = row[ i_total ].to_f
|
|
87
|
+
value = ( total == 0 ? 0.0 : value / total )
|
|
88
|
+
return ("%.2f" % value).to_f
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_link_column
|
|
92
|
+
i_start = cohort.column_index( :start_date )
|
|
93
|
+
add_column( :link, :report )
|
|
94
|
+
|
|
95
|
+
rows.each do |row|
|
|
96
|
+
start_date = row[ i_start ]
|
|
97
|
+
row << cohort_link( start_date )
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cohort_link( start_date, link_attributes = {} )
|
|
102
|
+
@cohort.param( :start_date ).value = start_date
|
|
103
|
+
@cohort.param( :stop_date ).value = param( :stop_date ).value
|
|
104
|
+
@cohort.param( :period ).value = param( :period ).value
|
|
105
|
+
@cohort.back = attributes
|
|
106
|
+
|
|
107
|
+
return @cohort.attributes.merge( link_attributes )
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module ReportCat
|
|
2
|
+
module Reports
|
|
3
|
+
include ReportCat::Core
|
|
4
|
+
|
|
5
|
+
class DateRangeReport < Report
|
|
6
|
+
|
|
7
|
+
PERIODS = [ :daily, :weekly, :monthly, :quarterly, :yearly ].freeze
|
|
8
|
+
|
|
9
|
+
def defaults
|
|
10
|
+
table_name = ReportCat::DateRange.table_name
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
:name => :date_range_report,
|
|
14
|
+
:from => table_name,
|
|
15
|
+
:order_by => "#{table_name}.start_date asc",
|
|
16
|
+
:group_by => "#{table_name}.start_date, #{table_name}.stop_date"
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize( attributes = {} )
|
|
21
|
+
super( defaults.merge( attributes ) )
|
|
22
|
+
|
|
23
|
+
add_param( :start_date, :date, Date.today - 7 )
|
|
24
|
+
add_param( :stop_date, :date, Date.today )
|
|
25
|
+
add_param( :period, :select, :weekly, :values => PERIODS )
|
|
26
|
+
|
|
27
|
+
table_name = ReportCat::DateRange.table_name
|
|
28
|
+
add_column( :start_date, :date, :sql => "#{table_name}.start_date" )
|
|
29
|
+
add_column( :stop_date, :date, :sql => "#{table_name}.stop_date" )
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def query
|
|
33
|
+
DateRange.generate( period, start_date, stop_date )
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def where
|
|
38
|
+
return [
|
|
39
|
+
super,
|
|
40
|
+
DateRange.sql_intersect( start_date, stop_date ),
|
|
41
|
+
DateRange.sql_period( period )
|
|
42
|
+
].compact.join( ' and ' )
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Accessors
|
|
46
|
+
|
|
47
|
+
def period
|
|
48
|
+
param( :period ).value.to_sym
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def start_date
|
|
52
|
+
param( :start_date ).value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stop_date
|
|
56
|
+
param( :stop_date ).value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def first_period
|
|
60
|
+
ReportCat::DateRange.range( period, start_date, stop_date ).first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/report_cat.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "report_cat/engine"
|
|
2
|
+
require "report_cat/config"
|
|
3
|
+
|
|
4
|
+
require 'report_cat/core/chart'
|
|
5
|
+
require 'report_cat/core/column'
|
|
6
|
+
require 'report_cat/core/param'
|
|
7
|
+
require 'report_cat/core/report'
|
|
8
|
+
|
|
9
|
+
require 'report_cat/reports/date_range_report'
|
|
10
|
+
require 'report_cat/reports/cohort_report'
|
|
11
|
+
|
|
12
|
+
if defined?( RSpec )
|
|
13
|
+
require 'report_cat/matchers/have_chart'
|
|
14
|
+
require 'report_cat/matchers/have_column'
|
|
15
|
+
require 'report_cat/matchers/have_param'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ReportCat
|
|
19
|
+
|
|
20
|
+
def self.config
|
|
21
|
+
return ReportCat::Config.instance
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.configure
|
|
25
|
+
yield config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.reports
|
|
29
|
+
reports = HashWithIndifferentAccess.new
|
|
30
|
+
|
|
31
|
+
ReportCat::Core::Report.descendants.map do |klass|
|
|
32
|
+
report = klass.new
|
|
33
|
+
reports[ report.name.to_sym ] = report
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return reports
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|