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
|
@@ -4,14 +4,32 @@ require 'forwardable'
|
|
|
4
4
|
module ActiveReporting
|
|
5
5
|
class Report
|
|
6
6
|
AGGREGATE_FUNCTION_OPERATORS = {
|
|
7
|
-
eq:
|
|
8
|
-
gt:
|
|
9
|
-
gte:
|
|
10
|
-
lt:
|
|
11
|
-
lte:
|
|
7
|
+
eq: '=',
|
|
8
|
+
gt: '>',
|
|
9
|
+
gte: '>=',
|
|
10
|
+
lt: '<',
|
|
11
|
+
lte: '<='
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
|
+
DIMENSION_FILTER_ON_VALUES = [true, 'true'].freeze
|
|
15
|
+
DIMENSION_FILTER_OFF_VALUES = [false, 'false'].freeze
|
|
16
|
+
|
|
14
17
|
extend Forwardable
|
|
18
|
+
|
|
19
|
+
# Converts a metric filter's operators to symbols, raising if any operator is not supported
|
|
20
|
+
#
|
|
21
|
+
# @param metric_filter [Hash] operator (e.g. `:gt` or `'gt'`) to value
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
def self.normalize_metric_filter(metric_filter)
|
|
24
|
+
metric_filter.to_h.transform_keys do |operator|
|
|
25
|
+
next operator.to_sym if AGGREGATE_FUNCTION_OPERATORS.key?(operator.to_s.to_sym)
|
|
26
|
+
|
|
27
|
+
raise UnknownMetricFilterOperator,
|
|
28
|
+
"Unknown metric filter operator '#{operator}'. Valid operators are: " \
|
|
29
|
+
"#{AGGREGATE_FUNCTION_OPERATORS.keys.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
def_delegators :@metric, :fact_model, :model
|
|
16
34
|
|
|
17
35
|
def initialize(metric, dimension_identifiers: true, dimension_filter: {}, dimensions: [], metric_filter: {})
|
|
@@ -21,9 +39,10 @@ module ActiveReporting
|
|
|
21
39
|
@dimension_identifiers = dimension_identifiers
|
|
22
40
|
local_dimensions = ReportingDimension.build_from_dimensions(fact_model, Array(dimensions))
|
|
23
41
|
@dimensions = (@metric.dimensions + local_dimensions).uniq
|
|
24
|
-
|
|
25
|
-
@
|
|
42
|
+
# The report's metric filter replaces the metric's, while the metric's dimension filters can't be overridden
|
|
43
|
+
@metric_filter = @metric.metric_filter.merge(Report.normalize_metric_filter(metric_filter))
|
|
26
44
|
partition_dimension_filters dimension_filter
|
|
45
|
+
@order_by = build_order_by
|
|
27
46
|
end
|
|
28
47
|
|
|
29
48
|
# Builds and executes a query, returning the raw result
|
|
@@ -36,13 +55,14 @@ module ActiveReporting
|
|
|
36
55
|
private ######################################################################
|
|
37
56
|
|
|
38
57
|
def build_data
|
|
39
|
-
@data = model.connection.
|
|
58
|
+
@data = model.connection.select_all(statement, 'ActiveReporting').to_a
|
|
40
59
|
apply_dimension_callbacks
|
|
41
60
|
@data
|
|
42
61
|
end
|
|
43
62
|
|
|
44
63
|
def partition_dimension_filters(user_dimension_filter)
|
|
45
64
|
@dimension_filters = { ransack: {}, scope: {}, lambda: {} }
|
|
65
|
+
# The metric's dimension filters are part of its definition, so they take precedence over the report's
|
|
46
66
|
user_dimension_filter.merge(@metric.dimension_filter).each do |key, value|
|
|
47
67
|
dm = fact_model.find_dimension_filter(key.to_sym)
|
|
48
68
|
@dimension_filters[dm.type][dm] = value
|
|
@@ -56,7 +76,7 @@ module ActiveReporting
|
|
|
56
76
|
left_outer_joins: dimension_joins(ReportingDimension::JOIN_METHODS[:left_outer_joins]),
|
|
57
77
|
group: group_by_statement,
|
|
58
78
|
having: having_statement,
|
|
59
|
-
order:
|
|
79
|
+
order: @order_by
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
statement = ([model] + parts.keys).inject do |chain, method|
|
|
@@ -65,13 +85,11 @@ module ActiveReporting
|
|
|
65
85
|
|
|
66
86
|
statement = process_scope_dimension_filter(statement)
|
|
67
87
|
statement = process_lambda_dimension_filter(statement)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
statement
|
|
88
|
+
process_ransack_dimension_filter(statement)
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
def select_statement
|
|
74
|
-
ss = ["#{select_aggregate} AS #{@metric.name}"]
|
|
92
|
+
ss = ["#{select_aggregate} AS #{model.connection.quote_column_name(@metric.name)}"]
|
|
75
93
|
ss += @dimensions.map { |d| d.select_statement(with_identifier: @dimension_identifiers) }
|
|
76
94
|
ss.flatten
|
|
77
95
|
end
|
|
@@ -81,36 +99,52 @@ module ActiveReporting
|
|
|
81
99
|
when :count
|
|
82
100
|
'COUNT(*)'
|
|
83
101
|
else
|
|
84
|
-
"#{@metric.aggregate.to_s.upcase}(#{
|
|
102
|
+
"#{@metric.aggregate.to_s.upcase}(#{measure_fragment})"
|
|
85
103
|
end
|
|
86
104
|
end
|
|
87
105
|
|
|
106
|
+
# The measure as a column qualified by the fact model's table, so it is never ambiguous with a column on
|
|
107
|
+
# a joined dimension. A measure given as `Arel.sql(...)` is used as is, allowing SQL expressions.
|
|
108
|
+
def measure_fragment
|
|
109
|
+
measure = @metric.measure || fact_model.measure
|
|
110
|
+
return measure if measure.is_a?(Arel::Nodes::SqlLiteral)
|
|
111
|
+
|
|
112
|
+
"#{model.quoted_table_name}.#{model.connection.quote_column_name(measure)}"
|
|
113
|
+
end
|
|
114
|
+
|
|
88
115
|
def dimension_joins(join_method)
|
|
89
|
-
@dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }
|
|
90
|
-
|
|
116
|
+
@dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }
|
|
117
|
+
.map { |d| d.name.to_sym }
|
|
91
118
|
end
|
|
92
119
|
|
|
93
120
|
def group_by_statement
|
|
94
121
|
@dimensions.map { |d| d.group_by_statement(with_identifier: @dimension_identifiers) }
|
|
95
122
|
end
|
|
96
123
|
|
|
124
|
+
# Rails defines scopes methods taking any arguments, so it can't be known if a scope takes an argument.
|
|
97
125
|
def process_scope_dimension_filter(chain)
|
|
98
|
-
@dimension_filters[:scope].each do |
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
@dimension_filters[:scope].each do |df, value|
|
|
127
|
+
next if DIMENSION_FILTER_OFF_VALUES.include?(value)
|
|
128
|
+
|
|
129
|
+
chain = if DIMENSION_FILTER_ON_VALUES.include?(value)
|
|
130
|
+
chain.public_send(df.name)
|
|
101
131
|
else
|
|
102
|
-
chain.public_send(
|
|
132
|
+
chain.public_send(df.name, value)
|
|
103
133
|
end
|
|
104
134
|
end
|
|
105
135
|
chain
|
|
106
136
|
end
|
|
107
137
|
|
|
138
|
+
# A lambda without parameters is a toggle: it is applied unless its value is `false`. A lambda with
|
|
139
|
+
# parameters is always passed the value, so it can take `true` and `false` as well.
|
|
108
140
|
def process_lambda_dimension_filter(chain)
|
|
109
|
-
@dimension_filters[:lambda].each do |df,
|
|
110
|
-
chain = if
|
|
111
|
-
chain.scoping { model.instance_exec(&df.body) }
|
|
141
|
+
@dimension_filters[:lambda].each do |df, value|
|
|
142
|
+
chain = if df.body.arity.nonzero?
|
|
143
|
+
chain.scoping { model.instance_exec(value, &df.body) }
|
|
144
|
+
elsif DIMENSION_FILTER_OFF_VALUES.include?(value)
|
|
145
|
+
chain
|
|
112
146
|
else
|
|
113
|
-
chain.scoping { model.instance_exec(
|
|
147
|
+
chain.scoping { model.instance_exec(&df.body) }
|
|
114
148
|
end
|
|
115
149
|
end
|
|
116
150
|
chain
|
|
@@ -121,8 +155,12 @@ module ActiveReporting
|
|
|
121
155
|
@dimension_filters[:ransack].each do |dm, value|
|
|
122
156
|
ransack_hash[dm.name] = value
|
|
123
157
|
end
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
return chain if ransack_hash.empty?
|
|
159
|
+
unless Configuration.ransack_available
|
|
160
|
+
raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
chain.ransack(ransack_hash).result
|
|
126
164
|
end
|
|
127
165
|
|
|
128
166
|
def having_statement
|
|
@@ -131,12 +169,16 @@ module ActiveReporting
|
|
|
131
169
|
end.join(' AND ')
|
|
132
170
|
end
|
|
133
171
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
@
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
def build_order_by
|
|
173
|
+
@metric.order_by_dimension.map do |dimension_key, direction|
|
|
174
|
+
dim = @dimensions.detect { |d| d.name.to_sym == dimension_key.to_sym }
|
|
175
|
+
if dim.nil?
|
|
176
|
+
raise UnknownDimension,
|
|
177
|
+
"Cannot order by '#{dimension_key}'; it is not one of the report's dimensions " \
|
|
178
|
+
"(#{@dimensions.map(&:name).join(', ')})"
|
|
139
179
|
end
|
|
180
|
+
|
|
181
|
+
dim.order_by_statement(direction: direction)
|
|
140
182
|
end
|
|
141
183
|
end
|
|
142
184
|
|
|
@@ -144,7 +186,8 @@ module ActiveReporting
|
|
|
144
186
|
@dimensions.each do |dimension|
|
|
145
187
|
callback = dimension.label_callback
|
|
146
188
|
next unless callback
|
|
147
|
-
|
|
189
|
+
|
|
190
|
+
key = dimension.label_name.to_s
|
|
148
191
|
@data.each do |hash|
|
|
149
192
|
hash[key] = callback.call(hash[key])
|
|
150
193
|
end
|
|
@@ -4,13 +4,17 @@ require 'forwardable'
|
|
|
4
4
|
module ActiveReporting
|
|
5
5
|
class ReportingDimension
|
|
6
6
|
extend Forwardable
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
# Values for the Postgres `date_trunc` method.
|
|
9
|
-
# See https://www.postgresql.org/docs/
|
|
9
|
+
# See https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
|
|
10
10
|
DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
|
|
11
11
|
century millennium date].freeze
|
|
12
|
+
# Column types a datetime drill can be used on. PostgreSQL reports `timestamp with time zone` columns as
|
|
13
|
+
# `timestamptz`, and `timestamp without time zone` columns as `timestamp` when its `datetime_type` is
|
|
14
|
+
# `:timestamptz`.
|
|
15
|
+
DATETIME_DRILL_COLUMN_TYPES = %i[datetime timestamp timestamptz date].freeze
|
|
12
16
|
JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
|
|
13
|
-
attr_reader :join_method, :label
|
|
17
|
+
attr_reader :join_method, :label, :label_name
|
|
14
18
|
|
|
15
19
|
def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
|
|
16
20
|
|
|
@@ -19,15 +23,17 @@ module ActiveReporting
|
|
|
19
23
|
dimension_name, options = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
|
|
20
24
|
found_dimension = fact_model.dimensions[dimension_name.to_sym]
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
if found_dimension.nil?
|
|
27
|
+
raise(UnknownDimension,
|
|
28
|
+
"Dimension '#{dimension_name}' not found on fact model '#{fact_model}'")
|
|
29
|
+
end
|
|
23
30
|
|
|
24
|
-
#
|
|
25
|
-
if !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
options = { datetime_drill: options }
|
|
31
|
+
# The `{ created_at: :month }` shorthand for datetime drills was removed in 0.7.0
|
|
32
|
+
if options && !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
|
|
33
|
+
raise InvalidDimensionLabel,
|
|
34
|
+
"'#{dimension_name}' is not hierarchical. For a datetime drill, use " \
|
|
35
|
+
"`{ #{dimension_name}: { datetime_drill: :#{options} } }` " \
|
|
36
|
+
"instead of `{ #{dimension_name}: :#{options} }`"
|
|
31
37
|
end
|
|
32
38
|
new(found_dimension, **label_config(options))
|
|
33
39
|
end
|
|
@@ -39,9 +45,7 @@ module ActiveReporting
|
|
|
39
45
|
#
|
|
40
46
|
# @param [Symbol|Hash] options
|
|
41
47
|
def self.label_config(options)
|
|
42
|
-
unless options.is_a?(Hash)
|
|
43
|
-
return { label: options }
|
|
44
|
-
end
|
|
48
|
+
return { label: options } unless options.is_a?(Hash)
|
|
45
49
|
|
|
46
50
|
{
|
|
47
51
|
label: options[:field],
|
|
@@ -75,7 +79,7 @@ module ActiveReporting
|
|
|
75
79
|
# @return [Array]
|
|
76
80
|
def select_statement(with_identifier: true)
|
|
77
81
|
ss = ["#{label_fragment} AS #{label_fragment_alias}"]
|
|
78
|
-
ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier &&
|
|
82
|
+
ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier && identifiable?
|
|
79
83
|
ss
|
|
80
84
|
end
|
|
81
85
|
|
|
@@ -84,7 +88,7 @@ module ActiveReporting
|
|
|
84
88
|
# @return [Array]
|
|
85
89
|
def group_by_statement(with_identifier: true)
|
|
86
90
|
group = [label_fragment]
|
|
87
|
-
group << identifier_fragment if with_identifier &&
|
|
91
|
+
group << identifier_fragment if with_identifier && identifiable?
|
|
88
92
|
group
|
|
89
93
|
end
|
|
90
94
|
|
|
@@ -93,36 +97,52 @@ module ActiveReporting
|
|
|
93
97
|
# @return [String]
|
|
94
98
|
def order_by_statement(direction:)
|
|
95
99
|
direction = direction.to_s.upcase
|
|
96
|
-
|
|
100
|
+
unless %w[ASC DESC].include?(direction)
|
|
101
|
+
raise InvalidOrderDirection,
|
|
102
|
+
"Ordering direction for #{name} should be 'asc' or 'desc', got '#{direction.downcase}'"
|
|
103
|
+
end
|
|
104
|
+
|
|
97
105
|
"#{label_fragment} #{direction}"
|
|
98
106
|
end
|
|
99
107
|
|
|
108
|
+
# Tells if the dimension's identifier is included in queries when the report asks for identifiers.
|
|
109
|
+
#
|
|
110
|
+
# Only a standard dimension labeled by its default label has one row per record. Grouping a dimension
|
|
111
|
+
# drilled to a hierarchy level or datetime by its identifier as well would split each roll-up back into one
|
|
112
|
+
# row per record.
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def identifiable?
|
|
116
|
+
type == Dimension::TYPES[:standard] && @datetime_drill.nil? && @label == dimension_fact_model.dimension_label
|
|
117
|
+
end
|
|
118
|
+
|
|
100
119
|
# Looks up the dimension label callback for the label
|
|
101
120
|
#
|
|
102
121
|
# @return [Lambda, NilClass]
|
|
103
122
|
def label_callback
|
|
104
|
-
klass.fact_model.dimension_label_callbacks[@label]
|
|
123
|
+
klass.fact_model.dimension_label_callbacks[@label.to_sym]
|
|
105
124
|
end
|
|
106
125
|
|
|
107
126
|
private ####################################################################
|
|
108
127
|
|
|
109
128
|
def determine_label_field(label_field)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
validate_hierarchical_label(label_field) if label_field.present?
|
|
130
|
+
|
|
131
|
+
@label = if type == Dimension::TYPES[:degenerate]
|
|
113
132
|
name
|
|
133
|
+
elsif label_field.present?
|
|
134
|
+
label_field.to_sym
|
|
114
135
|
else
|
|
115
136
|
dimension_fact_model.dimension_label || Configuration.default_dimension_label
|
|
116
137
|
end
|
|
117
138
|
end
|
|
118
139
|
|
|
119
140
|
def determine_label_name(label_name)
|
|
120
|
-
|
|
121
141
|
if label_name
|
|
122
142
|
@label_name = label_name
|
|
123
143
|
else
|
|
124
144
|
@label_name = name
|
|
125
|
-
@label_name += "_#{@label}" if
|
|
145
|
+
@label_name += "_#{@label}" if type == Dimension::TYPES[:standard] && @label != :name
|
|
126
146
|
@label_name += "_#{@datetime_drill}" if @datetime_drill
|
|
127
147
|
end
|
|
128
148
|
@label_name
|
|
@@ -130,6 +150,7 @@ module ActiveReporting
|
|
|
130
150
|
|
|
131
151
|
def determine_datetime_drill(datetime_drill)
|
|
132
152
|
return unless datetime_drill
|
|
153
|
+
|
|
133
154
|
validate_supported_database_for_datetime_hierarchies
|
|
134
155
|
validate_against_datetime_hierarchies(datetime_drill)
|
|
135
156
|
validate_label_is_datetime
|
|
@@ -147,18 +168,19 @@ module ActiveReporting
|
|
|
147
168
|
end
|
|
148
169
|
|
|
149
170
|
def validate_hierarchical_label(hierarchical_label)
|
|
150
|
-
|
|
171
|
+
validate_dimension_is_hierarchical(hierarchical_label)
|
|
151
172
|
validate_against_fact_model_properties(hierarchical_label)
|
|
152
|
-
true
|
|
153
173
|
end
|
|
154
174
|
|
|
155
|
-
def
|
|
175
|
+
def validate_dimension_is_hierarchical(hierarchical_label)
|
|
156
176
|
return if hierarchical?
|
|
177
|
+
|
|
157
178
|
raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
|
|
158
179
|
end
|
|
159
180
|
|
|
160
181
|
def validate_supported_database_for_datetime_hierarchies
|
|
161
|
-
return if
|
|
182
|
+
return if datetime_drill_adapter
|
|
183
|
+
|
|
162
184
|
raise InvalidDimensionLabel,
|
|
163
185
|
"Cannot utilize datetime grouping for #{name}; " \
|
|
164
186
|
"database #{model.connection.adapter_name} is not supported"
|
|
@@ -166,67 +188,26 @@ module ActiveReporting
|
|
|
166
188
|
|
|
167
189
|
def validate_against_datetime_hierarchies(hierarchical_label)
|
|
168
190
|
return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
|
|
191
|
+
|
|
169
192
|
raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
|
|
170
193
|
end
|
|
171
194
|
|
|
172
195
|
def validate_label_is_datetime
|
|
173
|
-
return if dimension_fact_model.model.column_for_attribute(@label).type
|
|
174
|
-
|
|
196
|
+
return if DATETIME_DRILL_COLUMN_TYPES.include?(dimension_fact_model.model.column_for_attribute(@label).type)
|
|
197
|
+
|
|
198
|
+
raise InvalidDimensionLabel, "'#{@label}' is not a datetime or date column"
|
|
175
199
|
end
|
|
176
200
|
|
|
177
201
|
def validate_against_fact_model_properties(hierarchical_label)
|
|
178
202
|
return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
|
|
179
|
-
raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
|
|
180
|
-
end
|
|
181
203
|
|
|
182
|
-
|
|
183
|
-
if model.connection.adapter_name == "Mysql2"
|
|
184
|
-
datetime_drill_mysql(column)
|
|
185
|
-
else # Postgress
|
|
186
|
-
datetime_drill_postgress(column)
|
|
187
|
-
end
|
|
204
|
+
raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
|
|
188
205
|
end
|
|
189
206
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
when :date
|
|
193
|
-
"DATE('#{column}')"
|
|
194
|
-
else
|
|
195
|
-
"DATE_TRUNC('#{@datetime_drill}', #{column})"
|
|
196
|
-
end
|
|
197
|
-
end
|
|
207
|
+
def datetime_drill_adapter
|
|
208
|
+
return @datetime_drill_adapter if defined?(@datetime_drill_adapter)
|
|
198
209
|
|
|
199
|
-
|
|
200
|
-
case @datetime_drill.to_sym
|
|
201
|
-
when :microseconds
|
|
202
|
-
"MICROSECOND(#{column})"
|
|
203
|
-
when :milliseconds
|
|
204
|
-
"MICROSECOND(#{column}) DIV 1000"
|
|
205
|
-
when :second
|
|
206
|
-
"SECOND(#{column})"
|
|
207
|
-
when :minute
|
|
208
|
-
"MINUTE(#{column})"
|
|
209
|
-
when :hour
|
|
210
|
-
"HOUR(#{column})"
|
|
211
|
-
when :day
|
|
212
|
-
"DAY(#{column})"
|
|
213
|
-
when :week
|
|
214
|
-
"WEEKDAY(#{column})"
|
|
215
|
-
when :month
|
|
216
|
-
"MONTH(#{column})"
|
|
217
|
-
when :quarter
|
|
218
|
-
"QUARTER(#{column})"
|
|
219
|
-
when :year
|
|
220
|
-
"YEAR(#{column})"
|
|
221
|
-
when :decade
|
|
222
|
-
"YEAR(#{column}) DIV 10"
|
|
223
|
-
when :century
|
|
224
|
-
"YEAR(#{column}) DIV 100"
|
|
225
|
-
when :millennium
|
|
226
|
-
"YEAR(#{column}) DIV 1000"
|
|
227
|
-
when :date
|
|
228
|
-
"DATE(#{column})"
|
|
229
|
-
end
|
|
210
|
+
@datetime_drill_adapter = DatetimeDrill.adapter_for(model.connection)
|
|
230
211
|
end
|
|
231
212
|
|
|
232
213
|
def identifier_fragment
|
|
@@ -234,17 +215,17 @@ module ActiveReporting
|
|
|
234
215
|
end
|
|
235
216
|
|
|
236
217
|
def identifier_fragment_alias
|
|
237
|
-
|
|
218
|
+
model.connection.quote_column_name("#{name}_identifier").to_s
|
|
238
219
|
end
|
|
239
220
|
|
|
240
221
|
def label_fragment
|
|
241
222
|
fragment = "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
|
|
242
|
-
fragment =
|
|
223
|
+
fragment = DatetimeDrill.fragment(datetime_drill_adapter, @datetime_drill, fragment) if @datetime_drill
|
|
243
224
|
fragment
|
|
244
225
|
end
|
|
245
226
|
|
|
246
227
|
def label_fragment_alias
|
|
247
|
-
|
|
228
|
+
model.connection.quote_column_name(@label_name).to_s
|
|
248
229
|
end
|
|
249
230
|
|
|
250
231
|
def dimension_fact_model
|
data/lib/active_reporting.rb
CHANGED
|
@@ -3,43 +3,63 @@
|
|
|
3
3
|
require 'active_record'
|
|
4
4
|
require 'active_reporting/active_record_adaptor'
|
|
5
5
|
require 'active_reporting/configuration'
|
|
6
|
+
require 'active_reporting/datetime_drill'
|
|
6
7
|
require 'active_reporting/dimension'
|
|
7
8
|
require 'active_reporting/dimension_filter'
|
|
8
9
|
require 'active_reporting/metric'
|
|
9
10
|
require 'active_reporting/fact_model'
|
|
11
|
+
require 'active_reporting/fact_model_registry'
|
|
10
12
|
require 'active_reporting/report'
|
|
11
13
|
require 'active_reporting/reporting_dimension'
|
|
12
14
|
require 'active_reporting/version'
|
|
15
|
+
require 'active_reporting/railtie' if defined?(Rails::Railtie)
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
require 'ransack'
|
|
16
|
-
ActiveReporting::Configuration.ransack_available = true
|
|
17
|
-
rescue LoadError, StandardError
|
|
18
|
-
ActiveReporting::Configuration.ransack_available = false
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
ActiveRecord::Base.extend(ActiveReporting::ActiveRecordAdaptor)
|
|
17
|
+
ActiveSupport.on_load(:active_record) { extend ActiveReporting::ActiveRecordAdaptor }
|
|
22
18
|
|
|
23
19
|
module ActiveReporting
|
|
24
20
|
def self.fetch_metric(name)
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
klass_name = Configuration.metric_lookup_class
|
|
22
|
+
klass = klass_name.safe_constantize
|
|
23
|
+
if klass.nil?
|
|
27
24
|
raise BadMetricLookupClass,
|
|
28
|
-
"#{
|
|
29
|
-
'
|
|
25
|
+
"#{klass_name} not defined. Please define a class responsible for looking up a metric by name. " \
|
|
26
|
+
'You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`.'
|
|
30
27
|
end
|
|
31
|
-
unless klass.
|
|
32
|
-
raise BadMetricLookupClass, "#{
|
|
28
|
+
unless klass.respond_to?(:lookup)
|
|
29
|
+
raise BadMetricLookupClass, "#{klass_name} needs to define a class method called 'lookup'"
|
|
33
30
|
end
|
|
34
|
-
|
|
31
|
+
|
|
32
|
+
klass.lookup(name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class BadMetricLookupClass < StandardError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class InvalidDimensionLabel < StandardError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Subclasses RuntimeError, which was raised for invalid directions before this class existed
|
|
42
|
+
class InvalidOrderDirection < RuntimeError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class RansackNotAvailable < StandardError
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
class UnknownAggregate < StandardError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class UnknownDimension < StandardError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class UnknownDimensionFilter < StandardError
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class UnknownMetric < StandardError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class UnknownMetricFilterOperator < StandardError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class UnknownJoinMethod < StandardError
|
|
64
|
+
end
|
|
45
65
|
end
|