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