active_reporter 0.5.8

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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +14 -0
  3. data/README.md +436 -0
  4. data/Rakefile +23 -0
  5. data/lib/active_reporter.rb +26 -0
  6. data/lib/active_reporter/aggregator.rb +9 -0
  7. data/lib/active_reporter/aggregator/array.rb +14 -0
  8. data/lib/active_reporter/aggregator/average.rb +9 -0
  9. data/lib/active_reporter/aggregator/base.rb +73 -0
  10. data/lib/active_reporter/aggregator/count.rb +23 -0
  11. data/lib/active_reporter/aggregator/count_if.rb +23 -0
  12. data/lib/active_reporter/aggregator/max.rb +9 -0
  13. data/lib/active_reporter/aggregator/min.rb +9 -0
  14. data/lib/active_reporter/aggregator/ratio.rb +23 -0
  15. data/lib/active_reporter/aggregator/sum.rb +13 -0
  16. data/lib/active_reporter/calculator.rb +2 -0
  17. data/lib/active_reporter/calculator/base.rb +19 -0
  18. data/lib/active_reporter/calculator/ratio.rb +9 -0
  19. data/lib/active_reporter/dimension.rb +8 -0
  20. data/lib/active_reporter/dimension/base.rb +150 -0
  21. data/lib/active_reporter/dimension/bin.rb +123 -0
  22. data/lib/active_reporter/dimension/bin/set.rb +162 -0
  23. data/lib/active_reporter/dimension/bin/table.rb +43 -0
  24. data/lib/active_reporter/dimension/category.rb +29 -0
  25. data/lib/active_reporter/dimension/enum.rb +32 -0
  26. data/lib/active_reporter/dimension/number.rb +51 -0
  27. data/lib/active_reporter/dimension/time.rb +93 -0
  28. data/lib/active_reporter/evaluator.rb +2 -0
  29. data/lib/active_reporter/evaluator/base.rb +17 -0
  30. data/lib/active_reporter/evaluator/block.rb +15 -0
  31. data/lib/active_reporter/inflector.rb +8 -0
  32. data/lib/active_reporter/invalid_params_error.rb +4 -0
  33. data/lib/active_reporter/report.rb +102 -0
  34. data/lib/active_reporter/report/aggregation.rb +297 -0
  35. data/lib/active_reporter/report/definition.rb +195 -0
  36. data/lib/active_reporter/report/metrics.rb +75 -0
  37. data/lib/active_reporter/report/validation.rb +106 -0
  38. data/lib/active_reporter/serializer.rb +7 -0
  39. data/lib/active_reporter/serializer/base.rb +103 -0
  40. data/lib/active_reporter/serializer/csv.rb +22 -0
  41. data/lib/active_reporter/serializer/form_field.rb +134 -0
  42. data/lib/active_reporter/serializer/hash_table.rb +12 -0
  43. data/lib/active_reporter/serializer/highcharts.rb +200 -0
  44. data/lib/active_reporter/serializer/nested_hash.rb +11 -0
  45. data/lib/active_reporter/serializer/table.rb +21 -0
  46. data/lib/active_reporter/tracker.rb +2 -0
  47. data/lib/active_reporter/tracker/base.rb +15 -0
  48. data/lib/active_reporter/tracker/delta.rb +9 -0
  49. data/lib/active_reporter/version.rb +3 -0
  50. data/lib/tasks/active_reporter_tasks.rake +4 -0
  51. data/spec/acceptance/data_spec.rb +381 -0
  52. data/spec/active_reporter/aggregator_spec.rb +102 -0
  53. data/spec/active_reporter/dimension/base_spec.rb +102 -0
  54. data/spec/active_reporter/dimension/bin/set_spec.rb +83 -0
  55. data/spec/active_reporter/dimension/bin/table_spec.rb +47 -0
  56. data/spec/active_reporter/dimension/bin_spec.rb +77 -0
  57. data/spec/active_reporter/dimension/category_spec.rb +60 -0
  58. data/spec/active_reporter/dimension/enum_spec.rb +94 -0
  59. data/spec/active_reporter/dimension/number_spec.rb +71 -0
  60. data/spec/active_reporter/dimension/time_spec.rb +61 -0
  61. data/spec/active_reporter/report_spec.rb +597 -0
  62. data/spec/active_reporter/serializer/hash_table_spec.rb +45 -0
  63. data/spec/active_reporter/serializer/highcharts_spec.rb +113 -0
  64. data/spec/active_reporter/serializer/table_spec.rb +62 -0
  65. data/spec/dummy/README.rdoc +28 -0
  66. data/spec/dummy/Rakefile +6 -0
  67. data/spec/dummy/app/assets/config/manifest.js +0 -0
  68. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  69. data/spec/dummy/app/assets/stylesheets/application.css +26 -0
  70. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  71. data/spec/dummy/app/controllers/site_controller.rb +11 -0
  72. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  73. data/spec/dummy/app/models/author.rb +4 -0
  74. data/spec/dummy/app/models/comment.rb +4 -0
  75. data/spec/dummy/app/models/data_builder.rb +112 -0
  76. data/spec/dummy/app/models/post.rb +6 -0
  77. data/spec/dummy/app/models/post_report.rb +14 -0
  78. data/spec/dummy/app/views/layouts/application.html.erb +17 -0
  79. data/spec/dummy/app/views/site/report.html.erb +73 -0
  80. data/spec/dummy/bin/bundle +3 -0
  81. data/spec/dummy/bin/rails +4 -0
  82. data/spec/dummy/bin/rake +4 -0
  83. data/spec/dummy/bin/setup +29 -0
  84. data/spec/dummy/config.ru +4 -0
  85. data/spec/dummy/config/application.rb +26 -0
  86. data/spec/dummy/config/boot.rb +5 -0
  87. data/spec/dummy/config/database.yml +22 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +41 -0
  90. data/spec/dummy/config/environments/production.rb +79 -0
  91. data/spec/dummy/config/environments/test.rb +42 -0
  92. data/spec/dummy/config/initializers/assets.rb +11 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/session_store.rb +3 -0
  99. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  100. data/spec/dummy/config/locales/en.yml +23 -0
  101. data/spec/dummy/config/routes.rb +57 -0
  102. data/spec/dummy/config/secrets.yml +22 -0
  103. data/spec/dummy/db/migrate/20150714202319_add_dummy_models.rb +25 -0
  104. data/spec/dummy/db/schema.rb +43 -0
  105. data/spec/dummy/db/seeds.rb +1 -0
  106. data/spec/dummy/log/test.log +37033 -0
  107. data/spec/dummy/public/404.html +67 -0
  108. data/spec/dummy/public/422.html +67 -0
  109. data/spec/dummy/public/500.html +66 -0
  110. data/spec/dummy/public/favicon.ico +0 -0
  111. data/spec/factories/factories.rb +29 -0
  112. data/spec/spec_helper.rb +40 -0
  113. data/spec/support/float.rb +8 -0
  114. metadata +385 -0
@@ -0,0 +1,23 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActiveReporter'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ require 'rspec/core/rake_task'
20
+
21
+ RSpec::Core::RakeTask.new(:spec)
22
+
23
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ module ActiveReporter
2
+ class << self
3
+ def database_type
4
+ @database_type ||= case database_adapter
5
+ when /postgres/ then :postgres
6
+ when /mysql/ then :mysql
7
+ when /sqlite/ then :sqlite
8
+ else
9
+ raise "unsupported database #{database_adapter}"
10
+ end
11
+ end
12
+
13
+ def numeric?(value)
14
+ value.is_a?(Numeric) || value.is_a?(String) && value =~ /\A\d+(?:\.\d+)?\z/
15
+ end
16
+
17
+ private
18
+
19
+ def database_adapter
20
+ ActiveRecord::Base.connection_config[:adapter]
21
+ end
22
+ end
23
+ end
24
+
25
+ require 'deeply_enumerable'
26
+ Dir.glob(File.join(__dir__, 'active_reporter', '*/')).each { |file| require file.chomp('/') }
@@ -0,0 +1,9 @@
1
+ require 'active_reporter/aggregator/base'
2
+ require 'active_reporter/aggregator/array'
3
+ require 'active_reporter/aggregator/average'
4
+ require 'active_reporter/aggregator/count'
5
+ require 'active_reporter/aggregator/count_if'
6
+ require 'active_reporter/aggregator/max'
7
+ require 'active_reporter/aggregator/min'
8
+ require 'active_reporter/aggregator/ratio'
9
+ require 'active_reporter/aggregator/sum'
@@ -0,0 +1,14 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Array < ActiveReporter::Aggregator::Base
4
+ def aggregate(groups)
5
+ fail InvalidParamsError, 'array agg is only supported in Postgres' unless ActiveReporter.database_type == :postgres
6
+ super
7
+ end
8
+
9
+ def function
10
+ "ARRAY_AGG(#{expression})"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Average < ActiveReporter::Aggregator::Base
4
+ def function
5
+ "AVG(#{expression})"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,73 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Base
4
+ attr_reader :name, :report, :opts
5
+
6
+ def initialize(name, report, opts={})
7
+ @name = name
8
+ @report = report
9
+ @opts = opts
10
+ validate_params!
11
+ end
12
+
13
+ def sql_value_name
14
+ "_report_aggregator_#{name}"
15
+ end
16
+
17
+ def default_value
18
+ opts.fetch(:default_value, nil)
19
+ end
20
+
21
+ def aggregate(groups)
22
+ relate(groups).select("#{function} AS #{sql_value_name}")
23
+ end
24
+
25
+ private
26
+
27
+ def validate_params!
28
+ if opts.include?(:expression)
29
+ ActiveSupport::Deprecation.warn("passing an :expression option will be deprecated in version 1.0\n please use :attribute, and, when required, :model or :table_name")
30
+ end
31
+ end
32
+
33
+ def relate(groups)
34
+ relation.call(groups)
35
+ end
36
+
37
+ def relation
38
+ opts.fetch(:relation, ->(r) { r })
39
+ end
40
+
41
+ def model
42
+ opts.fetch(:model, report.report_model)
43
+ end
44
+
45
+ def attribute
46
+ opts.fetch(:attribute, name)
47
+ end
48
+
49
+ def table_name
50
+ return @table_name unless @table_name.nil?
51
+
52
+ @table_name = opts[:table_name]
53
+ @table_name = model.try(:table_name) if @table_name.nil?
54
+ @table_name = model.to_s.constantize.try(:table_name) rescue nil if @table_name.nil?
55
+ @table_name = report.table_name if @table_name.nil?
56
+
57
+ @table_name
58
+ end
59
+
60
+ def column
61
+ opts.fetch(:column, attribute)
62
+ end
63
+
64
+ def expression
65
+ opts.fetch(:expression, "#{table_name}.#{column}")
66
+ end
67
+
68
+ def enum?
69
+ false # Hash(model&.defined_enums).include?(attribute.to_s)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Count < ActiveReporter::Aggregator::Base
4
+ def function
5
+ "COUNT(#{'DISTINCT' if distinct} #{expression})"
6
+ end
7
+
8
+ def default_value
9
+ super || 0
10
+ end
11
+
12
+ private
13
+
14
+ def distinct
15
+ opts[:distinct] || true
16
+ end
17
+
18
+ def column
19
+ opts.fetch(:column, 'id')
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class CountIf < ActiveReporter::Aggregator::Count
4
+ def function
5
+ "COUNT(#{expression} IN (#{values.map(&:to_s).join(',')}) OR NULL)"
6
+ end
7
+
8
+ def default_value
9
+ super || 0
10
+ end
11
+
12
+ private
13
+
14
+ def values
15
+ Array(opts[:values] || opts[:value] || true).compact
16
+ end
17
+
18
+ def column
19
+ super || 'id'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Max < ActiveReporter::Aggregator::Base
4
+ def function
5
+ "MAX(#{expression})"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Min < ActiveReporter::Aggregator::Base
4
+ def function
5
+ "MIN(#{expression})"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Ratio < ActiveReporter::Aggregator::Base
4
+ attr_reader :numerator, :denominator
5
+
6
+ def function
7
+ "(#{numerator}/NULLIF(#{denominator},0))"
8
+ end
9
+
10
+ private
11
+
12
+ def numerator
13
+ raise "Ratio aggregator must specify a numerator column" unless opts.include?(:numerator)
14
+ @numerator = report.aggregators[opts[:numerator].to_sym].try(:function) || "#{report.table_name}.#{opts[:numerator]}"
15
+ end
16
+
17
+ def denominator
18
+ raise "Ratio aggregator must specify a denominator column" unless opts.include?(:denominator)
19
+ @denominator = report.aggregators[opts[:denominator].to_sym].try(:function) || "#{report.table_name}.#{opts[:denominator]}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveReporter
2
+ module Aggregator
3
+ class Sum < ActiveReporter::Aggregator::Base
4
+ def function
5
+ "SUM(#{expression})"
6
+ end
7
+
8
+ def default_value
9
+ super || 0
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_reporter/calculator/base'
2
+ require 'active_reporter/calculator/ratio'
@@ -0,0 +1,19 @@
1
+ require 'active_reporter/aggregator/base'
2
+
3
+ module ActiveReporter
4
+ module Calculator
5
+ class Base < ActiveReporter::Aggregator::Base
6
+ def aggregator
7
+ opts[:aggregator] || name
8
+ end
9
+
10
+ def parent_aggregator
11
+ opts[:parent_aggregator] || aggregator
12
+ end
13
+
14
+ def totals?
15
+ !!opts[:totals]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveReporter
2
+ module Calculator
3
+ class Ratio < ActiveReporter::Calculator::Base
4
+ def calculate(row, parent_row)
5
+ ((row[aggregator].to_f / parent_row[parent_aggregator].to_f) * 100) unless parent_row[parent_aggregator].to_f.zero?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_reporter/dimension/base'
2
+ require 'active_reporter/dimension/bin'
3
+ require 'active_reporter/dimension/bin/set'
4
+ require 'active_reporter/dimension/bin/table'
5
+ require 'active_reporter/dimension/time'
6
+ require 'active_reporter/dimension/number'
7
+ require 'active_reporter/dimension/category'
8
+ require 'active_reporter/dimension/enum'
@@ -0,0 +1,150 @@
1
+ module ActiveReporter
2
+ module Dimension
3
+ class Base
4
+ attr_reader :name, :report, :opts
5
+
6
+ def initialize(name, report, opts={})
7
+ @name = name
8
+ @report = report
9
+ @opts = opts
10
+ validate_params!
11
+ end
12
+
13
+ def model
14
+ return @model unless @model.nil?
15
+
16
+ @model = opts[:model].to_s.classify.constantize rescue opts[:model]
17
+ @model = report.report_model if @model.nil?
18
+
19
+ @model
20
+ end
21
+
22
+ def attribute
23
+ opts.fetch(:attribute, name)
24
+ end
25
+
26
+ def expression
27
+ @expression ||= opts[:expression] || opts[:_expression] || "#{table_name}.#{column}"
28
+ end
29
+
30
+ # Do any joins/selects necessary to filter or group the relation.
31
+ def relate(relation)
32
+ opts.fetch(:relation, ->(r) { r }).call(relation)
33
+ end
34
+
35
+ # Filter the relation based on any constraints in the params
36
+ def filter(relation)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Group the relation by the expression -- ensure this is ordered, too.
41
+ def group(relation)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ # Return an ordered array of all values that should appear in `Report#data`
46
+ def group_values
47
+ raise NotImplementedError
48
+ end
49
+
50
+ # Given a single (hashified) row of the SQL result, return the Ruby
51
+ # object representing this dimension's value
52
+ def extract_sql_value(row)
53
+ sanitize_sql_value(row[sql_value_name])
54
+ end
55
+
56
+ def filter_values
57
+ array_param(:only).uniq
58
+ end
59
+
60
+ # Return whether the report should filter by this dimension
61
+ def filtering?
62
+ filter_values.present?
63
+ end
64
+
65
+ def grouping?
66
+ report.groupers.include?(self)
67
+ end
68
+
69
+ def order_expression
70
+ sql_value_name
71
+ end
72
+
73
+ def order(relation)
74
+ relation.order("#{order_expression} #{sort_order} #{null_order}")
75
+ end
76
+
77
+ def sort_desc?
78
+ dimension_or_root_param(:sort_desc)
79
+ end
80
+
81
+ def sort_order
82
+ sort_desc? ? 'DESC' : 'ASC'
83
+ end
84
+
85
+ def nulls_last?
86
+ value = dimension_or_root_param(:nulls_last)
87
+ value = !value if sort_desc?
88
+ value
89
+ end
90
+
91
+ def null_order
92
+ return unless ActiveReporter.database_type == :postgres
93
+ nulls_last? ? 'NULLS LAST' : 'NULLS FIRST'
94
+ end
95
+
96
+ def params
97
+ report.params.fetch(:dimensions, {})[name].presence || {}
98
+ end
99
+
100
+ private
101
+
102
+ def validate_params!
103
+ if opts.include?(:expression)
104
+ ActiveSupport::Deprecation.warn("passing an :expression option will be deprecated in version 1.0\n please use :attribute, and, when required, :model or :table_name")
105
+ end
106
+ end
107
+
108
+ def invalid_param!(param_key, message)
109
+ raise InvalidParamsError, "Invalid value for params[:dimensions] [:#{name}][:#{param_key}]: #{message}"
110
+ end
111
+
112
+ def table_name
113
+ return @table_name unless @table_name.nil?
114
+
115
+ @table_name = opts[:table_name]
116
+ @table_name = model.try(:table_name) if @table_name.nil?
117
+ @table_name = model.to_s.constantize.try(:table_name) rescue nil if @table_name.nil?
118
+ @table_name = report.table_name if @table_name.nil?
119
+
120
+ @table_name
121
+ end
122
+
123
+ def column
124
+ opts.fetch(:column, attribute)
125
+ end
126
+
127
+ def sql_value_name
128
+ "_active_reporter_dimension_#{name}"
129
+ end
130
+
131
+ def sanitize_sql_value(value)
132
+ value
133
+ end
134
+
135
+ def dimension_or_root_param(key)
136
+ params.fetch(key, report.params[key])
137
+ end
138
+
139
+ def array_param(key)
140
+ return [] unless params.key?(key)
141
+ return [nil] if params[key].nil?
142
+ Array.wrap(params[key])
143
+ end
144
+
145
+ def enum?
146
+ false # Hash(model&.defined_enums).include?(attribute.to_s)
147
+ end
148
+ end
149
+ end
150
+ end