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
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'active_reporter/inflector'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
class Report
|
5
|
+
module Definition
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
METRICS = %i[aggregator calculator dimension tracker evaluator].collect do |type|
|
9
|
+
metrics = Dir.glob(File.join(__dir__, '..', type.to_s, '*.rb')).collect { |file| File.basename(file, '.rb') }.without(*%w[base bin]).collect(&:to_sym).sort.freeze
|
10
|
+
[type, const_set(type.to_s.upcase, metrics)]
|
11
|
+
end.to_h.sort.freeze
|
12
|
+
|
13
|
+
delegate :report_model, to: :class
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
# Dimensions define what we are reporting about. For example, some common dimensions would be the Name of the
|
17
|
+
# data being reported on and the Date it happened, such as the Author name and the Published date of blog posts
|
18
|
+
# in an online blog.
|
19
|
+
# These dimension configurations are the data points that a report can be grouped by, data in the group will
|
20
|
+
# all be aggregated together using the configured aggregators. Multiple dimension types are available for
|
21
|
+
# different grouping methods.
|
22
|
+
#
|
23
|
+
# Category dimensions will do a normal GROUP BY on one specific field, this can also be used in conjunction
|
24
|
+
# with a filter to limit the GROUP BY values returned.
|
25
|
+
#
|
26
|
+
# Bin dimensions group many rows into "bins" of a specified width, this is helpful when grouping by a datetime
|
27
|
+
# value by allowing you to set this bin width to :days or :months. In addition to the Time dimension you can
|
28
|
+
# use the Number dimensions with a bin, grouping numeric values into larger groups such as 10s or 100s.
|
29
|
+
def dimension(name, dimension_class, opts = {})
|
30
|
+
dimensions[name.to_sym] = { axis_class: dimension_class, opts: opts }
|
31
|
+
end
|
32
|
+
|
33
|
+
def dimensions
|
34
|
+
@dimensions ||= { totals: { axis_class: Dimension::Category, opts: { _expression: "'totals'" } } }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Aggregators calculate the statistical data for the report into each dimension group. After grouping the data
|
38
|
+
# the aggregators calculate the values to be reported on. For example, if we use the dimensions Author name and
|
39
|
+
# Published date of blog posts in an online blog, we can then use aggregators on the Likes value to get the Sum
|
40
|
+
# of all Likes on all posts for each Published date for each Author name. There are multiple ways to aggregate
|
41
|
+
# this data, and so multiple aggregator types are provided.
|
42
|
+
#
|
43
|
+
# Average aggregator would calculate the average value across all the data in the group.
|
44
|
+
#
|
45
|
+
# Sum aggregator would calculate the sum total of all values across all the data in the group.
|
46
|
+
#
|
47
|
+
# Additional aggregators are also available for many other calculation types
|
48
|
+
def aggregator(name, aggregator_class, opts = {})
|
49
|
+
aggregators[name.to_sym] = { axis_class: aggregator_class, opts: opts }
|
50
|
+
end
|
51
|
+
|
52
|
+
def aggregators
|
53
|
+
@aggregators ||= {}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Calculators are special aggregators that perform calculations between report dimension data and a parent
|
57
|
+
# report.
|
58
|
+
# This could be used when generating a drill-down report with more specific data based on a specific row of a
|
59
|
+
# different report, where the parent report has larger dimension bins or fewer dimensions defined. For example,
|
60
|
+
# a parent report could group by Author only, and aggregate total Likes across all blog posts, the child report
|
61
|
+
# could group by Author and Published to provided more specific details. A calculator could then calculate the
|
62
|
+
# Ratio of Likes on a specific Published date vs the parent report's total Likes for that same specific Author.
|
63
|
+
# It can also be used to calculate values between the report data and the Totals report data, where the Totals
|
64
|
+
# report is the aggregation of all the data in the report combined. For example, if the report groups by Author
|
65
|
+
# we can aggregate total Likes across all blog posts for each Author. The Totals report would aggregate total
|
66
|
+
# Likes across all blog posts for all Author. A calculator can calculate the ratio of Total Likes vs the
|
67
|
+
# Author's total Likes.
|
68
|
+
#
|
69
|
+
# A calculator only performs additional calculations on already aggregated data, so an :aggregator value
|
70
|
+
# matching the aggregator name must be passed in the opts. Additionally, you may optionally pass a
|
71
|
+
# :parent_aggregator if the name of this aggregator is different.
|
72
|
+
def calculator(name, calculator_class, opts = {})
|
73
|
+
calculators[name.to_sym] = { axis_class: calculator_class, opts: opts }
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculators
|
77
|
+
@calculators ||= {}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Trackers are special aggregators that perform calculations between sequential bin dimension data. In order to
|
81
|
+
# use a tracker the last dimension configured in your report must be a bin dimension that defines a bin width
|
82
|
+
# so the tracker can determine the bin sequence. If a bin dimension with a bin width is not the last dimension
|
83
|
+
# configured tracker data will not be calculated.
|
84
|
+
# Any other dimensions are also considered when tracking data, if the value in any other dimension value
|
85
|
+
# changes between two rows the tracker data is not calculated. For example, if dimensions Author and Published
|
86
|
+
# are configured and an aggregator to sum Likes is configured, a tracker to calculate Likes delta may also be
|
87
|
+
# used. Each Published date the delta will be calculated, as long as the previous row has a Published date
|
88
|
+
# sequentially immediately adjacent to the current row. If the bin with is date, the dates 2020/06/05 and
|
89
|
+
# 2020/06/06 are adjacent, but if there are no blog posts for 2020/06/07 then the dela will not be calculated
|
90
|
+
# on the 2020/06/08 row since 2020/06/06 is not adjacent. Additionally, when the Author changes no delta will
|
91
|
+
# be calculated, even if the Published date on the row is sequentially immediately adjacent.
|
92
|
+
#
|
93
|
+
# A tracker only performs additional calculations on already aggregated data, so an :aggregator value matching
|
94
|
+
# the aggregator name must be passed in the opts.
|
95
|
+
def tracker(name, tracker_class, opts = {})
|
96
|
+
trackers[name.to_sym] = { axis_class: tracker_class, opts: opts }
|
97
|
+
end
|
98
|
+
|
99
|
+
def trackers
|
100
|
+
@trackers ||= {}
|
101
|
+
end
|
102
|
+
|
103
|
+
# block_evaluator(:chargeback_ratio) { |row| supplemental_report_data.detect { |data| data[:id] == row[:id] }[:count] / row[:count] }
|
104
|
+
def evaluator(name, evaluator_class, opts = {})
|
105
|
+
raise 'needs block' unless opts.include?(:block)
|
106
|
+
evaluators[name.to_sym] = { axis_class: evaluator_class, opts: opts }
|
107
|
+
end
|
108
|
+
|
109
|
+
def evaluators
|
110
|
+
@evaluators ||= {}
|
111
|
+
end
|
112
|
+
|
113
|
+
def available_dimensions
|
114
|
+
dimensions.keys
|
115
|
+
end
|
116
|
+
alias_method :available_groupers, :available_dimensions
|
117
|
+
|
118
|
+
def available_aggregators
|
119
|
+
aggregators.keys + calculators.keys + trackers.keys
|
120
|
+
end
|
121
|
+
|
122
|
+
METRICS.each do |type, mertics|
|
123
|
+
mertics.each do |mertic|
|
124
|
+
class_eval <<-METRIC_HELPERS, __FILE__, __LINE__ + 1
|
125
|
+
def #{mertic}_#{type}(name, opts = {}, &block)
|
126
|
+
opts[:block] = block if block_given?
|
127
|
+
#{type}(name, #{(type.to_s + '/' + mertic.to_s.singularize(:_gem_active_reporter)).camelize.sub(/.*\./, "")}, opts)
|
128
|
+
end
|
129
|
+
METRIC_HELPERS
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def default_report_model
|
134
|
+
name.demodulize.sub(/Report$/, '').constantize
|
135
|
+
rescue NameError
|
136
|
+
raise $!, "#{$!} cannot be used as `report_on` class, please configure `report_on` in the report class", $!.backtrace
|
137
|
+
end
|
138
|
+
|
139
|
+
def default_model
|
140
|
+
name.demodulize.sub(/Report$/, '').constantize
|
141
|
+
end
|
142
|
+
|
143
|
+
def report_model
|
144
|
+
@report_model ||= default_model
|
145
|
+
end
|
146
|
+
|
147
|
+
def report_on(class_or_name)
|
148
|
+
@report_model = class_or_name.to_s.constantize
|
149
|
+
rescue NameError
|
150
|
+
raise $!, "#{$!} cannot be used as `report_on` class", $!.backtrace
|
151
|
+
end
|
152
|
+
|
153
|
+
# ensure subclasses gain any aggregators or dimensions defined on their parents
|
154
|
+
def inherited(subclass)
|
155
|
+
instance_values.each { |var, val| subclass.instance_variable_set(:"@#{var}", val.dup) }
|
156
|
+
end
|
157
|
+
|
158
|
+
# autoreporting will automatically define dimensions based on columns
|
159
|
+
def autoreport_on(class_or_name)
|
160
|
+
report_on class_or_name
|
161
|
+
report_model.columns.each(&method(:autoreport_column))
|
162
|
+
count_aggregator(:count) if aggregators.blank?
|
163
|
+
end
|
164
|
+
|
165
|
+
# can override this method to skip or change certain column declarations
|
166
|
+
def autoreport_column(column)
|
167
|
+
return if column.name == report_model.primary_key
|
168
|
+
|
169
|
+
name, reflection = report_model.reflections.find { |_, reflection| reflection.foreign_key == column.name }
|
170
|
+
case
|
171
|
+
when reflection.present?
|
172
|
+
column_name = (reflection.klass.column_names & autoreport_association_name_columns(reflection)).first
|
173
|
+
if column_name.present?
|
174
|
+
category_dimension name, expression: "#{reflection.klass.table_name}.#{column_name}", relation: ->(r) { r.joins(name) }
|
175
|
+
else
|
176
|
+
category_dimension column.name
|
177
|
+
end
|
178
|
+
when %i[datetime timestamp time date].include?(column.type)
|
179
|
+
time_dimension column.name
|
180
|
+
when %i[integer float decimal].include?(column.type)
|
181
|
+
number_dimension column.name
|
182
|
+
else
|
183
|
+
category_dimension column.name
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# override this to change which columns of the association are used to
|
188
|
+
# auto-label it
|
189
|
+
def autoreport_association_name_columns(reflection)
|
190
|
+
%w(name email title)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ActiveReporter
|
2
|
+
class Report
|
3
|
+
module Metrics
|
4
|
+
delegate :table_name, to: :report_model
|
5
|
+
|
6
|
+
def dimensions
|
7
|
+
@dimensions ||= build_axes(self.class.dimensions)
|
8
|
+
end
|
9
|
+
|
10
|
+
def aggregators
|
11
|
+
@aggregators ||= build_axes(self.class.aggregators.slice(*Array(params.fetch(:aggregators, self.class.aggregators.keys)).collect(&:to_sym)))
|
12
|
+
end
|
13
|
+
|
14
|
+
def grouper_names
|
15
|
+
names = params.fetch(:groupers, [dimensions.except(:totals).keys.first])
|
16
|
+
names = names.is_a?(Hash) ? names.values : Array.wrap(names).compact
|
17
|
+
names.map(&:to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
def groupers
|
21
|
+
@groupers ||= dimensions.values_at(*grouper_names)
|
22
|
+
end
|
23
|
+
|
24
|
+
def filters
|
25
|
+
@filters ||= dimensions.values.select(&:filtering?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def relators
|
29
|
+
filters | groupers
|
30
|
+
end
|
31
|
+
|
32
|
+
def base_relation
|
33
|
+
params.fetch(:relation, report_model.all)
|
34
|
+
end
|
35
|
+
|
36
|
+
def relation
|
37
|
+
@relation ||= relators.reduce(base_relation) { |relation, dimension| dimension.relate(relation) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def records
|
41
|
+
@records ||= filters.reduce(relation) { |relation, dimension| dimension.filter(relation) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def groups
|
45
|
+
@groups ||= groupers.reduce(records) { |relation, dimension| dimension.group(relation) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def calculators
|
49
|
+
@calculators ||= build_axes(self.class.calculators.slice(*Array(params[:calculators]).collect(&:to_sym)))
|
50
|
+
end
|
51
|
+
|
52
|
+
def trackers
|
53
|
+
@trackers ||= build_axes(self.class.trackers.slice(*Array(params[:trackers]).collect(&:to_sym)))
|
54
|
+
end
|
55
|
+
|
56
|
+
def evaluators
|
57
|
+
@evaluators ||= build_axes(self.class.evaluators.slice(*Array(params[:evaluators]).collect(&:to_sym)))
|
58
|
+
end
|
59
|
+
|
60
|
+
def fields
|
61
|
+
[groupers, calculators.keys, trackers.keys, evaluators.keys].inject(&:merge)
|
62
|
+
end
|
63
|
+
|
64
|
+
def total_report
|
65
|
+
@total_report ||= self.class.new(@params.except(:calculators).merge({ groupers: :totals })) unless @total_data.present?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def build_axes(axes)
|
71
|
+
axes.map { |name, h| [name, h[:axis_class].new(name, self, h[:opts])] }.to_h
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'active_reporter/inflector'
|
2
|
+
require 'active_reporter/invalid_params_error'
|
3
|
+
|
4
|
+
module ActiveReporter
|
5
|
+
class Report
|
6
|
+
module Validation
|
7
|
+
attr_accessor :errors
|
8
|
+
|
9
|
+
def validate_params!
|
10
|
+
validate_configuration!
|
11
|
+
validate_aggregators!
|
12
|
+
validate_groupers!
|
13
|
+
validate_calculators!
|
14
|
+
validate_trackers!
|
15
|
+
validate_parent_report!
|
16
|
+
validate_total_report!
|
17
|
+
|
18
|
+
raise_invalid_params_error! if errors.present? && errors.any?
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_configuration!
|
22
|
+
incomplete_message = ['You must declare at least one aggregator or tracker, and at lease one dimension to initialize a report', 'See the README for more details']
|
23
|
+
|
24
|
+
raise ActiveReporter::InvalidParamsError, ["#{self.class.name} does not declare any aggregators or trackers"].concat(incomplete_message).join(". ") if aggregators.empty?
|
25
|
+
raise ActiveReporter::InvalidParamsError, ["#{self.class.name} does not declare any dimensions"].concat(incomplete_message).join(". ") if dimensions.except(:totals).empty?
|
26
|
+
raise ActiveReporter::InvalidParamsError, 'parent_report must be included in order to process calculations' if calculators.any? && parent_report.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_aggregators!
|
30
|
+
(aggregators.keys - self.class.aggregators.keys).each do |aggregator|
|
31
|
+
add_invalid_param_error(:aggregator, ":#{aggregator} is not a valid aggregator (should be in #{self.class.aggregators.keys})")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_calculators!
|
36
|
+
(calculators.keys - self.class.calculators.keys).each do |calculator|
|
37
|
+
add_invalid_param_error(:calculator, ":#{calculator} is not a valid calculator (should be in #{self.class.calculators.keys})")
|
38
|
+
end
|
39
|
+
|
40
|
+
calculators.values.each do |calculator|
|
41
|
+
if calculator.aggregator.nil?
|
42
|
+
add_invalid_param_error(:calculator, ":#{calculator.name} must define an aggregator (should be in #{self.class.aggregator.keys})")
|
43
|
+
elsif aggregators.exclude?(calculator.aggregator)
|
44
|
+
add_invalid_param_error(:calculator, ":#{calculator.name} defines an valid aggregator :#{calculator.aggregator} (should be in #{self.class.aggregators.keys})")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_trackers!
|
50
|
+
(trackers.keys - self.class.trackers.keys).each do |tracker|
|
51
|
+
add_invalid_param_error(:tracker, ":#{tracker} is not a valid tracker (should be in #{self.class.trackers.keys})")
|
52
|
+
end
|
53
|
+
|
54
|
+
trackers.values.each do |tracker|
|
55
|
+
if tracker.aggregator.nil?
|
56
|
+
add_invalid_param_error(:tracker, ":#{tracker.name} must define an aggregator (should be in #{self.class.aggregator.keys})")
|
57
|
+
elsif aggregators.exclude?(tracker.aggregator)
|
58
|
+
add_invalid_param_error(:tracker, ":#{tracker.name} defines an valid aggregator :#{tracker.aggregator} (should be in #{self.class.aggregators.keys})")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_groupers!
|
64
|
+
unless groupers.all?(&:present?)
|
65
|
+
invalid_groupers = grouper_names.zip(groupers).collect { |k,v| k if v.nil? }.compact
|
66
|
+
invalid_groupers_message = [
|
67
|
+
[
|
68
|
+
invalid_groupers.to_sentence,
|
69
|
+
(invalid_groupers.one? ? 'is not a' : 'are not'), 'valid', 'dimension'.pluralize(invalid_groupers.count, :_gem_active_reporter)
|
70
|
+
].join(' '),
|
71
|
+
"declared dimension include #{dimensions.keys.to_sentence}"
|
72
|
+
].join(". ")
|
73
|
+
add_invalid_param_error(:groupers, invalid_groupers_message)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_parent_report!
|
78
|
+
add_invalid_param_error(:parent_report, 'must be an instance of ActiveReporter::Report') unless parent_report.nil? || parent_report.kind_of?(ActiveReporter::Report)
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_total_report!
|
82
|
+
add_invalid_param_error(:total_report, 'must be an instance of ActiveReporter::Report') unless @total_report.nil? || @total_report.kind_of?(ActiveReporter::Report)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def add_error(message)
|
88
|
+
self.errors ||= []
|
89
|
+
self.errors.push(message)
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_invalid_param_error(param_key, message)
|
93
|
+
self.errors ||= []
|
94
|
+
self.errors.push("Invalid value for params[:#{param_key}]: #{message}")
|
95
|
+
end
|
96
|
+
|
97
|
+
def raise_invalid_params_error!
|
98
|
+
raise ActiveReporter::InvalidParamsError, error_message
|
99
|
+
end
|
100
|
+
|
101
|
+
def error_message
|
102
|
+
(["The report configuration contains the following #{'error'.pluralize(errors.count, :_gem_active_reporter)}:"] + errors).join("\n - ")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'active_reporter/serializer/base'
|
2
|
+
require 'active_reporter/serializer/table'
|
3
|
+
require 'active_reporter/serializer/csv'
|
4
|
+
require 'active_reporter/serializer/form_field'
|
5
|
+
require 'active_reporter/serializer/highcharts'
|
6
|
+
require 'active_reporter/serializer/hash_table'
|
7
|
+
require 'active_reporter/serializer/nested_hash'
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'active_reporter/inflector'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
module Serializer
|
5
|
+
class Base
|
6
|
+
attr_reader :report
|
7
|
+
|
8
|
+
def initialize(report)
|
9
|
+
@report = report
|
10
|
+
end
|
11
|
+
|
12
|
+
# Consider overriding many of these methods to use I18n with keys based
|
13
|
+
# on the aggregators or dimension name.
|
14
|
+
|
15
|
+
def human_aggregator_label(aggregators)
|
16
|
+
aggregators.keys.collect { |aggregator| aggregator.to_s.humanize }.join(' ')
|
17
|
+
end
|
18
|
+
|
19
|
+
def human_dimension_label(dimension)
|
20
|
+
dimension.name.to_s.humanize
|
21
|
+
end
|
22
|
+
|
23
|
+
def human_null_value_label(dimension)
|
24
|
+
"No #{human_dimension_label(dimension)}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def human_aggregator_value_label(aggregator, value)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
def human_dimension_value_label(dimension, value)
|
32
|
+
return human_null_value_label(dimension) if value.nil?
|
33
|
+
|
34
|
+
case dimension
|
35
|
+
when ActiveReporter::Dimension::Category
|
36
|
+
human_category_value_label(dimension, value)
|
37
|
+
when ActiveReporter::Dimension::Number
|
38
|
+
human_number_value_label(dimension, value)
|
39
|
+
when ActiveReporter::Dimension::Time
|
40
|
+
human_time_value_label(dimension, value)
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def human_category_value_label(dimension, value)
|
47
|
+
value
|
48
|
+
end
|
49
|
+
|
50
|
+
def human_number_value_label(dimension, value)
|
51
|
+
case value.bin_edges
|
52
|
+
when :min_and_max
|
53
|
+
"[#{value.min.round(2)}, #{value.max.round(2)})"
|
54
|
+
when :min
|
55
|
+
">= #{value.min.round(2)}"
|
56
|
+
when :max
|
57
|
+
"< #{value.max.round(2)}"
|
58
|
+
else
|
59
|
+
human_null_value_label(dimension)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def time_formats
|
64
|
+
{
|
65
|
+
minutes: '%F %k:%M', hours: '%F %k', days: '%F',
|
66
|
+
weeks: 'week of %F', months: '%Y-%m', years: '%Y'
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def human_time_value_label(dimension, value)
|
71
|
+
case value.bin_edges
|
72
|
+
when :min_and_max
|
73
|
+
time_formats.each { |step, format| return value.min.strftime(format) if value.max == value.min.advance(step => 1) } || "#{value.min} to #{value.max}"
|
74
|
+
when :min
|
75
|
+
"after #{value.min}"
|
76
|
+
when :max
|
77
|
+
"before #{value.max}"
|
78
|
+
else
|
79
|
+
human_null_value_label(dimension)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def record_type
|
84
|
+
report.table_name.singularize(:_gem_active_reporter).humanize
|
85
|
+
end
|
86
|
+
|
87
|
+
def axis_summary
|
88
|
+
y = human_aggregator_label(report.aggregators)
|
89
|
+
xes = report.groupers.map(&method(:human_dimension_label))
|
90
|
+
count = "#{report.records.count} #{record_type.pluralize(report.records.count, :_gem_active_reporter)}"
|
91
|
+
"#{y} by #{xes.to_sentence} for #{count}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def filter_summary
|
95
|
+
report.filters.flat_map do |dimension|
|
96
|
+
human_dimension_label(dimension) + " = " + dimension.filter_values.map do |value|
|
97
|
+
human_dimension_value_label(dimension, value)
|
98
|
+
end.to_sentence(last_word_connector: ', or ')
|
99
|
+
end.join('; ')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|