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.
- 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
|