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