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