active_reporter 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
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,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