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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +147 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/javascripts/report_cat/application.js +13 -0
  6. data/app/assets/stylesheets/report_cat/application.css +13 -0
  7. data/app/controllers/report_cat/reports_controller.rb +43 -0
  8. data/app/helpers/report_cat/reports_helper.rb +132 -0
  9. data/app/models/report_cat/date_range.rb +94 -0
  10. data/app/views/report_cat/reports/_google_charts.html.erb +61 -0
  11. data/app/views/report_cat/reports/index.html.erb +2 -0
  12. data/app/views/report_cat/reports/show.html.erb +15 -0
  13. data/config/locales/en.yml +26 -0
  14. data/config/routes.rb +7 -0
  15. data/db/migrate/20130918075200_create_date_ranges.rb +13 -0
  16. data/lib/report_cat/config.rb +30 -0
  17. data/lib/report_cat/core/chart.rb +50 -0
  18. data/lib/report_cat/core/column.rb +70 -0
  19. data/lib/report_cat/core/param.rb +35 -0
  20. data/lib/report_cat/core/report.rb +127 -0
  21. data/lib/report_cat/engine.rb +11 -0
  22. data/lib/report_cat/matchers/have_chart.rb +58 -0
  23. data/lib/report_cat/matchers/have_column.rb +45 -0
  24. data/lib/report_cat/matchers/have_param.rb +52 -0
  25. data/lib/report_cat/reports/cohort_report.rb +113 -0
  26. data/lib/report_cat/reports/date_range_report.rb +66 -0
  27. data/lib/report_cat/version.rb +3 -0
  28. data/lib/report_cat.rb +39 -0
  29. data/lib/tasks/report_cat.rake +4 -0
  30. data/spec/controllers/report_cat/reports_controller_spec.rb +100 -0
  31. data/spec/coverage_spec.rb +18 -0
  32. data/spec/data/helpers/report_charts.html +1 -0
  33. data/spec/data/helpers/report_charts.html.tmp +1 -0
  34. data/spec/data/helpers/report_form.html +63 -0
  35. data/spec/data/helpers/report_form.html.tmp +63 -0
  36. data/spec/data/helpers/report_form_param.html +1 -0
  37. data/spec/data/helpers/report_form_param.html.tmp +1 -0
  38. data/spec/data/helpers/report_list.html +1 -0
  39. data/spec/data/helpers/report_list.html.tmp +1 -0
  40. data/spec/data/helpers/report_param_checkbox.html +1 -0
  41. data/spec/data/helpers/report_param_checkbox.html.tmp +1 -0
  42. data/spec/data/helpers/report_param_date.html +60 -0
  43. data/spec/data/helpers/report_param_date.html.tmp +60 -0
  44. data/spec/data/helpers/report_param_hidden.html +1 -0
  45. data/spec/data/helpers/report_param_hidden.html.tmp +1 -0
  46. data/spec/data/helpers/report_param_select.html +3 -0
  47. data/spec/data/helpers/report_param_select.html.tmp +3 -0
  48. data/spec/data/helpers/report_param_text_field.html +1 -0
  49. data/spec/data/helpers/report_param_text_field.html.tmp +1 -0
  50. data/spec/data/helpers/report_table.html +1 -0
  51. data/spec/data/helpers/report_table.html.tmp +1 -0
  52. data/spec/data/helpers/report_table_hidden.html +1 -0
  53. data/spec/data/helpers/report_table_hidden.html.tmp +1 -0
  54. data/spec/data/lib/chart_columns.json +1 -0
  55. data/spec/data/lib/chart_columns.json.tmp +1 -0
  56. data/spec/data/lib/chart_data.json +1 -0
  57. data/spec/data/lib/chart_data.json.tmp +1 -0
  58. data/spec/data/lib/date_range_report_where.sql +6 -0
  59. data/spec/data/lib/date_range_report_where.sql.tmp +6 -0
  60. data/spec/data/lib/report.csv +3 -0
  61. data/spec/data/lib/report.csv.tmp +3 -0
  62. data/spec/data/lib/report.sql +1 -0
  63. data/spec/data/lib/report.sql.tmp +1 -0
  64. data/spec/data/models/sql_intersect.sql +5 -0
  65. data/spec/data/models/sql_intersect.sql.tmp +5 -0
  66. data/spec/dummy/README.rdoc +28 -0
  67. data/spec/dummy/Rakefile +6 -0
  68. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  69. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  70. data/spec/dummy/app/controllers/application_controller.rb +21 -0
  71. data/spec/dummy/app/controllers/root_controller.rb +20 -0
  72. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  73. data/spec/dummy/app/models/user.rb +13 -0
  74. data/spec/dummy/app/models/visit.rb +16 -0
  75. data/spec/dummy/app/reports/retention_cohort_report.rb +19 -0
  76. data/spec/dummy/app/reports/retention_report.rb +30 -0
  77. data/spec/dummy/app/reports/user_report.rb +23 -0
  78. data/spec/dummy/app/views/layouts/admin.html.erb +19 -0
  79. data/spec/dummy/app/views/layouts/application.html.erb +19 -0
  80. data/spec/dummy/app/views/root/index.html.erb +8 -0
  81. data/spec/dummy/bin/bundle +3 -0
  82. data/spec/dummy/bin/rails +4 -0
  83. data/spec/dummy/bin/rake +4 -0
  84. data/spec/dummy/config/application.rb +30 -0
  85. data/spec/dummy/config/boot.rb +5 -0
  86. data/spec/dummy/config/database.yml +25 -0
  87. data/spec/dummy/config/environment.rb +5 -0
  88. data/spec/dummy/config/environments/development.rb +31 -0
  89. data/spec/dummy/config/environments/production.rb +80 -0
  90. data/spec/dummy/config/environments/test.rb +36 -0
  91. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  92. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  93. data/spec/dummy/config/initializers/inflections.rb +16 -0
  94. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  95. data/spec/dummy/config/initializers/report_cat.rb +15 -0
  96. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  97. data/spec/dummy/config/initializers/session_store.rb +3 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +38 -0
  100. data/spec/dummy/config/routes.rb +13 -0
  101. data/spec/dummy/config.ru +4 -0
  102. data/spec/dummy/db/development.sqlite3 +0 -0
  103. data/spec/dummy/db/schema.rb +26 -0
  104. data/spec/dummy/db/test.sqlite3 +0 -0
  105. data/spec/dummy/log/development.log +61 -0
  106. data/spec/dummy/log/test.log +26478 -0
  107. data/spec/dummy/public/404.html +58 -0
  108. data/spec/dummy/public/422.html +58 -0
  109. data/spec/dummy/public/500.html +57 -0
  110. data/spec/dummy/public/favicon.ico +0 -0
  111. data/spec/helpers/report_cat/reports_helper_spec.rb +224 -0
  112. data/spec/lib/report_cat/config_spec.rb +96 -0
  113. data/spec/lib/report_cat/core/chart_spec.rb +67 -0
  114. data/spec/lib/report_cat/core/column_spec.rb +156 -0
  115. data/spec/lib/report_cat/core/param_spec.rb +95 -0
  116. data/spec/lib/report_cat/core/report_spec.rb +342 -0
  117. data/spec/lib/report_cat/engine_spec.rb +9 -0
  118. data/spec/lib/report_cat/matchers/have_chart_spec.rb +36 -0
  119. data/spec/lib/report_cat/matchers/have_column_spec.rb +30 -0
  120. data/spec/lib/report_cat/matchers/have_param_spec.rb +33 -0
  121. data/spec/lib/report_cat/reports/cohort_report_spec.rb +215 -0
  122. data/spec/lib/report_cat/reports/date_range_report_spec.rb +125 -0
  123. data/spec/lib/report_cat/version_spec.rb +11 -0
  124. data/spec/lib/report_cat_spec.rb +62 -0
  125. data/spec/lib/tasks/report_cat.rake_spec.rb +13 -0
  126. data/spec/models/report_cat/date_range_spec.rb +144 -0
  127. data/spec/rails_helper.rb +49 -0
  128. data/spec/spec_helper.rb +23 -0
  129. data/spec/support/setup_reports.rb +28 -0
  130. data/spec/views/report_cat/reports/index.html.erb_spec.rb +16 -0
  131. data/spec/views/report_cat/reports/show.html.erb_spec.rb +19 -0
  132. 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,11 @@
1
+ module ReportCat
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ReportCat
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.integration_tool :rspec
8
+ end
9
+
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module ReportCat
2
+ VERSION = '0.2.0'
3
+ 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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :report_cat do
3
+ # # Task goes here
4
+ # end