active_reporter 0.5.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +14 -0
- data/README.md +436 -0
- data/Rakefile +23 -0
- data/lib/active_reporter.rb +26 -0
- data/lib/active_reporter/aggregator.rb +9 -0
- data/lib/active_reporter/aggregator/array.rb +14 -0
- data/lib/active_reporter/aggregator/average.rb +9 -0
- data/lib/active_reporter/aggregator/base.rb +73 -0
- data/lib/active_reporter/aggregator/count.rb +23 -0
- data/lib/active_reporter/aggregator/count_if.rb +23 -0
- data/lib/active_reporter/aggregator/max.rb +9 -0
- data/lib/active_reporter/aggregator/min.rb +9 -0
- data/lib/active_reporter/aggregator/ratio.rb +23 -0
- data/lib/active_reporter/aggregator/sum.rb +13 -0
- data/lib/active_reporter/calculator.rb +2 -0
- data/lib/active_reporter/calculator/base.rb +19 -0
- data/lib/active_reporter/calculator/ratio.rb +9 -0
- data/lib/active_reporter/dimension.rb +8 -0
- data/lib/active_reporter/dimension/base.rb +150 -0
- data/lib/active_reporter/dimension/bin.rb +123 -0
- data/lib/active_reporter/dimension/bin/set.rb +162 -0
- data/lib/active_reporter/dimension/bin/table.rb +43 -0
- data/lib/active_reporter/dimension/category.rb +29 -0
- data/lib/active_reporter/dimension/enum.rb +32 -0
- data/lib/active_reporter/dimension/number.rb +51 -0
- data/lib/active_reporter/dimension/time.rb +93 -0
- data/lib/active_reporter/evaluator.rb +2 -0
- data/lib/active_reporter/evaluator/base.rb +17 -0
- data/lib/active_reporter/evaluator/block.rb +15 -0
- data/lib/active_reporter/inflector.rb +8 -0
- data/lib/active_reporter/invalid_params_error.rb +4 -0
- data/lib/active_reporter/report.rb +102 -0
- data/lib/active_reporter/report/aggregation.rb +297 -0
- data/lib/active_reporter/report/definition.rb +195 -0
- data/lib/active_reporter/report/metrics.rb +75 -0
- data/lib/active_reporter/report/validation.rb +106 -0
- data/lib/active_reporter/serializer.rb +7 -0
- data/lib/active_reporter/serializer/base.rb +103 -0
- data/lib/active_reporter/serializer/csv.rb +22 -0
- data/lib/active_reporter/serializer/form_field.rb +134 -0
- data/lib/active_reporter/serializer/hash_table.rb +12 -0
- data/lib/active_reporter/serializer/highcharts.rb +200 -0
- data/lib/active_reporter/serializer/nested_hash.rb +11 -0
- data/lib/active_reporter/serializer/table.rb +21 -0
- data/lib/active_reporter/tracker.rb +2 -0
- data/lib/active_reporter/tracker/base.rb +15 -0
- data/lib/active_reporter/tracker/delta.rb +9 -0
- data/lib/active_reporter/version.rb +3 -0
- data/lib/tasks/active_reporter_tasks.rake +4 -0
- data/spec/acceptance/data_spec.rb +381 -0
- data/spec/active_reporter/aggregator_spec.rb +102 -0
- data/spec/active_reporter/dimension/base_spec.rb +102 -0
- data/spec/active_reporter/dimension/bin/set_spec.rb +83 -0
- data/spec/active_reporter/dimension/bin/table_spec.rb +47 -0
- data/spec/active_reporter/dimension/bin_spec.rb +77 -0
- data/spec/active_reporter/dimension/category_spec.rb +60 -0
- data/spec/active_reporter/dimension/enum_spec.rb +94 -0
- data/spec/active_reporter/dimension/number_spec.rb +71 -0
- data/spec/active_reporter/dimension/time_spec.rb +61 -0
- data/spec/active_reporter/report_spec.rb +597 -0
- data/spec/active_reporter/serializer/hash_table_spec.rb +45 -0
- data/spec/active_reporter/serializer/highcharts_spec.rb +113 -0
- data/spec/active_reporter/serializer/table_spec.rb +62 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +26 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/site_controller.rb +11 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/author.rb +4 -0
- data/spec/dummy/app/models/comment.rb +4 -0
- data/spec/dummy/app/models/data_builder.rb +112 -0
- data/spec/dummy/app/models/post.rb +6 -0
- data/spec/dummy/app/models/post_report.rb +14 -0
- data/spec/dummy/app/views/layouts/application.html.erb +17 -0
- data/spec/dummy/app/views/site/report.html.erb +73 -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/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -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 +4 -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 +23 -0
- data/spec/dummy/config/routes.rb +57 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/migrate/20150714202319_add_dummy_models.rb +25 -0
- data/spec/dummy/db/schema.rb +43 -0
- data/spec/dummy/db/seeds.rb +1 -0
- data/spec/dummy/log/test.log +37033 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/factories.rb +29 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/float.rb +8 -0
- metadata +385 -0
data/Rakefile
ADDED
@@ -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,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,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,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
|