active_reporting 0.6.1 → 1.0.0
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 +4 -4
- data/CHANGELOG.md +86 -30
- data/README.md +141 -45
- data/lib/active_reporting/active_record_adaptor.rb +2 -10
- data/lib/active_reporting/configuration.rb +63 -45
- data/lib/active_reporting/datetime_drill.rb +88 -0
- data/lib/active_reporting/dimension.rb +10 -6
- data/lib/active_reporting/fact_model.rb +15 -10
- data/lib/active_reporting/fact_model_registry.rb +51 -0
- data/lib/active_reporting/metric.rb +12 -7
- data/lib/active_reporting/railtie.rb +9 -0
- data/lib/active_reporting/report.rb +75 -32
- data/lib/active_reporting/reporting_dimension.rb +60 -79
- data/lib/active_reporting/version.rb +1 -1
- data/lib/active_reporting.rb +43 -23
- metadata +20 -122
- data/.codeclimate.yml +0 -15
- data/.gitignore +0 -12
- data/.rubocop.yml +0 -29
- data/.travis.yml +0 -20
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -15
- data/Rakefile +0 -12
- data/active_reporting.gemspec +0 -37
- data/bin/console +0 -15
- data/bin/setup +0 -8
|
@@ -2,22 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveReporting
|
|
4
4
|
module Configuration
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
# The default label used by all dimensions if not set otherwise
|
|
6
|
+
#
|
|
7
|
+
# Default value is `:name`
|
|
8
|
+
#
|
|
9
|
+
# @return [Symbol]
|
|
10
|
+
mattr_reader :default_dimension_label, default: :name
|
|
11
|
+
|
|
12
|
+
# The default measure for all fact models
|
|
13
|
+
#
|
|
14
|
+
# Default value is `:value`
|
|
15
|
+
#
|
|
16
|
+
# @return [Symbol]
|
|
17
|
+
mattr_reader :default_measure, default: :value
|
|
18
|
+
|
|
19
|
+
# Tells if unknown dimension filters should always fallback to ransack
|
|
20
|
+
#
|
|
21
|
+
# Default value is `false`
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
mattr_reader :ransack_fallback, default: false
|
|
25
|
+
|
|
26
|
+
# Returns the name of the constant used to lookup prebuilt `ActiveReporting::Metric`
|
|
27
|
+
# objects by name. The constant should define a class method called `#lookup`
|
|
28
|
+
# which can take a string or symbol of the metric name.
|
|
29
|
+
#
|
|
30
|
+
# Default value is `::Metric`
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
mattr_reader :metric_lookup_class, default: '::Metric'
|
|
11
34
|
|
|
12
35
|
def self.config
|
|
13
36
|
yield self
|
|
14
37
|
end
|
|
15
38
|
|
|
16
|
-
#
|
|
39
|
+
# `mattr_reader` stores values in class variables, so the custom writers below set them directly.
|
|
40
|
+
# Configuration is never included into other classes, so class variable sharing is not a concern.
|
|
41
|
+
# rubocop:disable Style/ClassVars
|
|
42
|
+
|
|
43
|
+
# Determines if ransack is available for use in the gem. Ransack is loaded the first time this is
|
|
44
|
+
# called, so it does not matter whether it is required before or after ActiveReporting.
|
|
17
45
|
#
|
|
18
|
-
#
|
|
19
|
-
def self.
|
|
20
|
-
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def self.ransack_available
|
|
48
|
+
return @@ransack_available if defined?(@@ransack_available)
|
|
49
|
+
|
|
50
|
+
@@ransack_available = begin
|
|
51
|
+
require 'ransack'
|
|
52
|
+
true
|
|
53
|
+
rescue LoadError
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Overrides ransack detection
|
|
59
|
+
#
|
|
60
|
+
# @param available [Boolean]
|
|
61
|
+
def self.ransack_available=(available)
|
|
62
|
+
@@ransack_available = available
|
|
21
63
|
end
|
|
22
64
|
|
|
23
65
|
# Sets the default dimension label to be used by all dimensions
|
|
@@ -25,58 +67,34 @@ module ActiveReporting
|
|
|
25
67
|
# @param dimension_label [String, Symbol]
|
|
26
68
|
# @return [Symbol]
|
|
27
69
|
def self.default_dimension_label=(dimension_label)
|
|
28
|
-
|
|
70
|
+
@@default_dimension_label = dimension_label.to_sym
|
|
29
71
|
end
|
|
30
72
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Default value is `:value``
|
|
34
|
-
def self.default_measure
|
|
35
|
-
@default_measure ||= :value
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Sets the default measture to be used by all fact models
|
|
73
|
+
# Sets the default measure to be used by all fact models
|
|
39
74
|
#
|
|
40
75
|
# @param measure [String, Symbol]
|
|
41
76
|
# @return [Symbol]
|
|
42
77
|
def self.default_measure=(measure)
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Tells if unkown dimension filters should always fallback to ransack
|
|
47
|
-
#
|
|
48
|
-
# Default value is `false`
|
|
49
|
-
#
|
|
50
|
-
# @return [Boolean]
|
|
51
|
-
def self.ransack_fallback
|
|
52
|
-
@ransack_fallback ||= false
|
|
78
|
+
@@default_measure = measure.to_sym
|
|
53
79
|
end
|
|
54
80
|
|
|
55
81
|
# Sets the flag to always fallback to ransack for unknown dimension filters
|
|
82
|
+
#
|
|
56
83
|
# @param fallback [Boolean]
|
|
57
84
|
# @return [Boolean]
|
|
58
85
|
def self.ransack_fallback=(fallback)
|
|
59
|
-
raise RansackNotAvailable
|
|
60
|
-
|
|
86
|
+
raise RansackNotAvailable if fallback && !ransack_available
|
|
87
|
+
|
|
88
|
+
@@ransack_fallback = fallback
|
|
61
89
|
end
|
|
62
90
|
|
|
63
|
-
# Sets the name of the constant used to lookup prebuilt `
|
|
91
|
+
# Sets the name of the constant used to lookup prebuilt `ActiveReporting::Metric`
|
|
64
92
|
# objects by name.
|
|
65
93
|
#
|
|
66
|
-
# @param klass_name [String]
|
|
94
|
+
# @param klass_name [String, Symbol, Class]
|
|
67
95
|
def self.metric_lookup_class=(klass_name)
|
|
68
|
-
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Sets the name of the constant used to lookup prebuilt `Reporting::Metric`
|
|
72
|
-
# objects by name. The constant should define a class method called `#lookup`
|
|
73
|
-
# which can take a string or symbol of the metric name.
|
|
74
|
-
#
|
|
75
|
-
# Default value is ::Metric
|
|
76
|
-
#
|
|
77
|
-
# @returns [String]
|
|
78
|
-
def self.metric_lookup_class
|
|
79
|
-
@metric_lookup_class ||= '::Metric'
|
|
96
|
+
@@metric_lookup_class = "::#{klass_name.to_s.camelize.delete_prefix('::')}"
|
|
80
97
|
end
|
|
98
|
+
# rubocop:enable Style/ClassVars
|
|
81
99
|
end
|
|
82
100
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveReporting
|
|
4
|
+
# Builds the SQL used to drill a datetime column to a given level of a datetime hierarchy
|
|
5
|
+
module DatetimeDrill
|
|
6
|
+
# Adapters that support datetime drills, keyed by the adapter class to look for in the connection's
|
|
7
|
+
# ancestry. Matching on ancestry covers adapters built on top of these, e.g. PostGIS and Trilogy.
|
|
8
|
+
ADAPTERS = {
|
|
9
|
+
'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter' => :postgresql,
|
|
10
|
+
'ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter' => :mysql,
|
|
11
|
+
'ActiveRecord::ConnectionAdapters::SQLite3Adapter' => :sqlite
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# The datetime drill flavor for a connection, or nil if datetime drills are not supported for it
|
|
17
|
+
#
|
|
18
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
19
|
+
# @return [Symbol, nil] :postgresql, :mysql, or :sqlite
|
|
20
|
+
def adapter_for(connection)
|
|
21
|
+
connection.class.ancestors.filter_map { |ancestor| ADAPTERS[ancestor.name] }.first
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param adapter [Symbol] as returned by `adapter_for`
|
|
25
|
+
# @param drill [Symbol, String] one of `ReportingDimension::DATETIME_HIERARCHIES`
|
|
26
|
+
# @param column [String] the quoted column to drill
|
|
27
|
+
# @return [String] SQL fragment
|
|
28
|
+
def fragment(adapter, drill, column)
|
|
29
|
+
public_send(adapter, drill, column)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def postgresql(drill, column)
|
|
33
|
+
case drill.to_sym
|
|
34
|
+
when :date
|
|
35
|
+
"DATE(#{column})"
|
|
36
|
+
else
|
|
37
|
+
"DATE_TRUNC('#{drill}', #{column})"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Mirrors Postgres' `date_trunc`, returning the truncated value as a DATETIME. Values are built from
|
|
42
|
+
# formatted strings because casting to a lower fractional precision rounds instead of truncating.
|
|
43
|
+
def mysql(drill, column)
|
|
44
|
+
year = "YEAR(#{column})"
|
|
45
|
+
sql = case drill.to_sym
|
|
46
|
+
when :microseconds then return column
|
|
47
|
+
when :milliseconds
|
|
48
|
+
return "CAST(LEFT(DATE_FORMAT(#{column}, '%Y-%m-%d %H:%i:%s.%f'), 23) AS DATETIME(3))"
|
|
49
|
+
when :second then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:%i:%s')"
|
|
50
|
+
when :minute then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:%i:00')"
|
|
51
|
+
when :hour then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:00:00')"
|
|
52
|
+
when :day then "DATE_FORMAT(#{column}, '%Y-%m-%d 00:00:00')"
|
|
53
|
+
when :week then "DATE_SUB(DATE(#{column}), INTERVAL WEEKDAY(#{column}) DAY)"
|
|
54
|
+
when :month then "DATE_FORMAT(#{column}, '%Y-%m-01 00:00:00')"
|
|
55
|
+
when :quarter then "MAKEDATE(#{year}, 1) + INTERVAL (QUARTER(#{column}) - 1) QUARTER"
|
|
56
|
+
when :year then "MAKEDATE(#{year}, 1)"
|
|
57
|
+
when :decade then "MAKEDATE(#{year} DIV 10 * 10, 1)"
|
|
58
|
+
when :century then "MAKEDATE((#{year} - 1) DIV 100 * 100 + 1, 1)"
|
|
59
|
+
when :millennium then "MAKEDATE((#{year} - 1) DIV 1000 * 1000 + 1, 1)"
|
|
60
|
+
when :date then return "DATE(#{column})"
|
|
61
|
+
end
|
|
62
|
+
"CAST(#{sql} AS DATETIME)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Mirrors Postgres' `date_trunc`, returning the truncated datetime as 'YYYY-MM-DD HH:MM:SS' text
|
|
66
|
+
def sqlite(drill, column)
|
|
67
|
+
year = "CAST(STRFTIME('%Y', #{column}) AS INTEGER)"
|
|
68
|
+
case drill.to_sym
|
|
69
|
+
when :microseconds then column
|
|
70
|
+
# STRFTIME's %f rounds to the nearest millisecond, so truncate the stored fraction directly
|
|
71
|
+
when :milliseconds then "STRFTIME('%Y-%m-%d %H:%M:%S', #{column}) || SUBSTR(#{column} || '.000', 20, 4)"
|
|
72
|
+
when :second then "STRFTIME('%Y-%m-%d %H:%M:%S', #{column})"
|
|
73
|
+
when :minute then "STRFTIME('%Y-%m-%d %H:%M:00', #{column})"
|
|
74
|
+
when :hour then "STRFTIME('%Y-%m-%d %H:00:00', #{column})"
|
|
75
|
+
when :day then "STRFTIME('%Y-%m-%d 00:00:00', #{column})"
|
|
76
|
+
when :week then "STRFTIME('%Y-%m-%d 00:00:00', #{column}, '-6 days', 'weekday 1')"
|
|
77
|
+
when :month then "STRFTIME('%Y-%m-01 00:00:00', #{column})"
|
|
78
|
+
when :quarter
|
|
79
|
+
"PRINTF('%04d-%02d-01 00:00:00', #{year}, (CAST(STRFTIME('%m', #{column}) AS INTEGER) - 1) / 3 * 3 + 1)"
|
|
80
|
+
when :year then "STRFTIME('%Y-01-01 00:00:00', #{column})"
|
|
81
|
+
when :decade then "PRINTF('%04d-01-01 00:00:00', #{year} / 10 * 10)"
|
|
82
|
+
when :century then "PRINTF('%04d-01-01 00:00:00', (#{year} - 1) / 100 * 100 + 1)"
|
|
83
|
+
when :millennium then "PRINTF('%04d-01-01 00:00:00', (#{year} - 1) / 1000 * 1000 + 1)"
|
|
84
|
+
when :date then "DATE(#{column})"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -5,14 +5,14 @@ module ActiveReporting
|
|
|
5
5
|
TYPES = { degenerate: :degenerate, standard: :standard }.freeze
|
|
6
6
|
attr_reader :name
|
|
7
7
|
|
|
8
|
-
# @param
|
|
8
|
+
# @param fact_model [ActiveReporting::FactModel]
|
|
9
9
|
# @param name [Symbol]
|
|
10
10
|
def initialize(fact_model, name:)
|
|
11
11
|
@fact_model = fact_model
|
|
12
12
|
@name = name.to_s
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Determines the type of the dimension
|
|
16
16
|
#
|
|
17
17
|
# A dimension type is either:
|
|
18
18
|
#
|
|
@@ -34,18 +34,20 @@ module ActiveReporting
|
|
|
34
34
|
#
|
|
35
35
|
# @return [Boolean]
|
|
36
36
|
def hierarchical?
|
|
37
|
-
@hierarchical
|
|
37
|
+
return @hierarchical if defined?(@hierarchical)
|
|
38
|
+
|
|
39
|
+
@hierarchical = !klass.fact_model.hierarchical_levels.empty?
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# Returns either the model of the dimension's association or the model
|
|
41
43
|
# itself if the dimension lives on the fact model
|
|
42
44
|
#
|
|
43
|
-
# @return [
|
|
45
|
+
# @return [Class]
|
|
44
46
|
def klass
|
|
45
47
|
@klass ||= association ? association.klass : model
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
# Returns the fact model's
|
|
50
|
+
# Returns the fact model's ActiveRecord model
|
|
49
51
|
#
|
|
50
52
|
# @return [ActiveRecord::Base]
|
|
51
53
|
def model
|
|
@@ -56,7 +58,9 @@ module ActiveReporting
|
|
|
56
58
|
#
|
|
57
59
|
# @return [ActiveRecord::Reflection]
|
|
58
60
|
def association
|
|
59
|
-
@association
|
|
61
|
+
return @association if defined?(@association)
|
|
62
|
+
|
|
63
|
+
@association = model.reflect_on_association(@name)
|
|
60
64
|
end
|
|
61
65
|
end
|
|
62
66
|
end
|
|
@@ -7,7 +7,7 @@ module ActiveReporting
|
|
|
7
7
|
attr_writer :measure
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
# Explicitly sets which ActiveRecord model to
|
|
10
|
+
# Explicitly sets which ActiveRecord model to link to this fact model.
|
|
11
11
|
#
|
|
12
12
|
# @note You should only need to set this if the name of your fact model does not
|
|
13
13
|
# follow the pattern of [MyModel]FactModel
|
|
@@ -27,7 +27,7 @@ module ActiveReporting
|
|
|
27
27
|
else
|
|
28
28
|
model
|
|
29
29
|
end
|
|
30
|
-
|
|
30
|
+
FactModelRegistry.register(@model, self)
|
|
31
31
|
@model
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -104,7 +104,7 @@ module ActiveReporting
|
|
|
104
104
|
@dimension_label_callbacks ||= {}
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
# Sets a
|
|
107
|
+
# Sets a callback for a given dimension label. The returned value of
|
|
108
108
|
# the callable body will be used as the label value when used in a report.
|
|
109
109
|
# The label's raw database value is passed to the callback.
|
|
110
110
|
#
|
|
@@ -113,12 +113,13 @@ module ActiveReporting
|
|
|
113
113
|
def self.dimension_label_callback(column, body)
|
|
114
114
|
@dimension_label_callbacks ||= {}
|
|
115
115
|
raise ArgumentError, 'Dimension label callback body must be a callable object' unless body.respond_to?(:call)
|
|
116
|
+
|
|
116
117
|
@dimension_label_callbacks[column.to_sym] = body
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
# Declares a dimension filter for this fact model
|
|
120
121
|
#
|
|
121
|
-
# @param name [
|
|
122
|
+
# @param name [String, Symbol] The name of the dimension filter
|
|
122
123
|
# @param lambda_or_type [Symbol, Lambda]
|
|
123
124
|
#
|
|
124
125
|
# @note If not provided, the type of the dimension filter will be a scope.
|
|
@@ -127,6 +128,10 @@ module ActiveReporting
|
|
|
127
128
|
# dimension filter is a ransack search term. Finally, you may pass in
|
|
128
129
|
# a callable object similar to defining a scope on ActiveRecord
|
|
129
130
|
#
|
|
131
|
+
# @note A report applies a scope, or a lambda without parameters, for `true` and leaves it out for
|
|
132
|
+
# `false`. A lambda with parameters is always passed the report's value. See "Dimension filter values"
|
|
133
|
+
# in the README.
|
|
134
|
+
#
|
|
130
135
|
# @example
|
|
131
136
|
# class PostFactModel < ActiveReporting::FactModel
|
|
132
137
|
# # Assumes there's an `active` scope on the model
|
|
@@ -149,6 +154,7 @@ module ActiveReporting
|
|
|
149
154
|
unless Configuration.ransack_available
|
|
150
155
|
raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
|
|
151
156
|
end
|
|
157
|
+
|
|
152
158
|
@ransack_fallback = true
|
|
153
159
|
end
|
|
154
160
|
|
|
@@ -156,7 +162,6 @@ module ActiveReporting
|
|
|
156
162
|
#
|
|
157
163
|
# @return [Boolean]
|
|
158
164
|
def self.ransack_fallback
|
|
159
|
-
@ransack_fallback ||= false
|
|
160
165
|
@ransack_fallback || Configuration.ransack_fallback
|
|
161
166
|
end
|
|
162
167
|
private_class_method :ransack_fallback
|
|
@@ -166,11 +171,11 @@ module ActiveReporting
|
|
|
166
171
|
# @param name [Symbol]
|
|
167
172
|
# @return [ActiveReporting::DimensionFilter]
|
|
168
173
|
def self.find_dimension_filter(name)
|
|
169
|
-
@dimension_filters
|
|
170
|
-
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
raise UnknownDimensionFilter, "Dimension filter '#{name}' not found on fact model '#{self
|
|
174
|
+
dimension_filter = @dimension_filters&.fetch(name.to_sym, nil)
|
|
175
|
+
return dimension_filter if dimension_filter
|
|
176
|
+
return DimensionFilter.build(name, :ransack) if ransack_fallback
|
|
177
|
+
|
|
178
|
+
raise UnknownDimensionFilter, "Dimension filter '#{name}' not found on fact model '#{self}'"
|
|
174
179
|
end
|
|
175
180
|
end
|
|
176
181
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent/map'
|
|
4
|
+
|
|
5
|
+
module ActiveReporting
|
|
6
|
+
# Maps ActiveRecord models to their fact models.
|
|
7
|
+
#
|
|
8
|
+
# Lookups are cached here instead of on the model classes so the cache can be cleared when Rails
|
|
9
|
+
# reloads code (see ActiveReporting::Railtie). Otherwise a model could keep returning a fact model
|
|
10
|
+
# from before the reload, or a fact model could keep pointing to a model from before the reload.
|
|
11
|
+
module FactModelRegistry
|
|
12
|
+
@fact_models = Concurrent::Map.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Returns the fact model for an ActiveRecord model: the `[MyModel]FactModel` class if one is
|
|
16
|
+
# defined, otherwise a generated fact model linked to the model.
|
|
17
|
+
#
|
|
18
|
+
# @param model [Class] an ActiveRecord model
|
|
19
|
+
# @return [Class] an ActiveReporting::FactModel subclass
|
|
20
|
+
def fetch(model)
|
|
21
|
+
@fact_models.fetch_or_store(model) { "#{model.name}FactModel".safe_constantize || generate(model) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Links an ActiveRecord model to a fact model
|
|
25
|
+
#
|
|
26
|
+
# @param model [Class] an ActiveRecord model
|
|
27
|
+
# @param fact_model [Class] an ActiveReporting::FactModel subclass
|
|
28
|
+
def register(model, fact_model)
|
|
29
|
+
@fact_models[model] = fact_model
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Forgets all looked up and registered fact models
|
|
33
|
+
def clear
|
|
34
|
+
@fact_models.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Generated fact models are not assigned to a constant, so they never outlive a code reload or
|
|
40
|
+
# stop a `[MyModel]FactModel` defined later from being autoloaded.
|
|
41
|
+
def generate(model)
|
|
42
|
+
label = "#{model.name}FactModel (generated)"
|
|
43
|
+
Class.new(FactModel) do
|
|
44
|
+
define_singleton_method(:to_s) { label }
|
|
45
|
+
define_singleton_method(:inspect) { label }
|
|
46
|
+
self.model = model
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -6,8 +6,10 @@ module ActiveReporting
|
|
|
6
6
|
|
|
7
7
|
class Metric
|
|
8
8
|
extend Forwardable
|
|
9
|
+
|
|
9
10
|
def_delegators :@fact_model, :model
|
|
10
|
-
attr_reader :fact_model, :name, :dimensions, :dimension_filter, :aggregate
|
|
11
|
+
attr_reader :fact_model, :name, :dimensions, :dimension_filter, :aggregate
|
|
12
|
+
attr_reader :metric_filter, :order_by_dimension, :measure
|
|
11
13
|
|
|
12
14
|
def initialize(
|
|
13
15
|
name,
|
|
@@ -16,15 +18,17 @@ module ActiveReporting
|
|
|
16
18
|
dimensions: [],
|
|
17
19
|
dimension_filter: {},
|
|
18
20
|
metric_filter: {},
|
|
19
|
-
order_by_dimension: {}
|
|
21
|
+
order_by_dimension: {},
|
|
22
|
+
measure: nil
|
|
20
23
|
)
|
|
21
24
|
@name = name.to_sym
|
|
22
25
|
@fact_model = fact_model
|
|
23
26
|
@dimension_filter = dimension_filter
|
|
24
|
-
@aggregate =
|
|
25
|
-
@metric_filter = metric_filter
|
|
27
|
+
@aggregate = validate_aggregate(aggregate.to_sym)
|
|
28
|
+
@metric_filter = Report.normalize_metric_filter(metric_filter)
|
|
26
29
|
@dimensions = ReportingDimension.build_from_dimensions(@fact_model, Array(dimensions))
|
|
27
30
|
@order_by_dimension = order_by_dimension
|
|
31
|
+
@measure = measure
|
|
28
32
|
check_dimension_filter
|
|
29
33
|
end
|
|
30
34
|
|
|
@@ -38,14 +42,15 @@ module ActiveReporting
|
|
|
38
42
|
private ####################################################################
|
|
39
43
|
|
|
40
44
|
def check_dimension_filter
|
|
41
|
-
@dimension_filter.
|
|
45
|
+
@dimension_filter.each_key do |name|
|
|
42
46
|
@fact_model.find_dimension_filter(name)
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
def
|
|
50
|
+
def validate_aggregate(agg)
|
|
47
51
|
raise UnknownAggregate, "Unknown aggregate '#{agg}'" unless AGGREGATES.include?(agg)
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
agg
|
|
49
54
|
end
|
|
50
55
|
end
|
|
51
56
|
end
|