active_reporter 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +14 -0
- data/README.md +436 -0
- data/Rakefile +23 -0
- data/lib/active_reporter.rb +26 -0
- data/lib/active_reporter/aggregator.rb +9 -0
- data/lib/active_reporter/aggregator/array.rb +14 -0
- data/lib/active_reporter/aggregator/average.rb +9 -0
- data/lib/active_reporter/aggregator/base.rb +73 -0
- data/lib/active_reporter/aggregator/count.rb +23 -0
- data/lib/active_reporter/aggregator/count_if.rb +23 -0
- data/lib/active_reporter/aggregator/max.rb +9 -0
- data/lib/active_reporter/aggregator/min.rb +9 -0
- data/lib/active_reporter/aggregator/ratio.rb +23 -0
- data/lib/active_reporter/aggregator/sum.rb +13 -0
- data/lib/active_reporter/calculator.rb +2 -0
- data/lib/active_reporter/calculator/base.rb +19 -0
- data/lib/active_reporter/calculator/ratio.rb +9 -0
- data/lib/active_reporter/dimension.rb +8 -0
- data/lib/active_reporter/dimension/base.rb +150 -0
- data/lib/active_reporter/dimension/bin.rb +123 -0
- data/lib/active_reporter/dimension/bin/set.rb +162 -0
- data/lib/active_reporter/dimension/bin/table.rb +43 -0
- data/lib/active_reporter/dimension/category.rb +29 -0
- data/lib/active_reporter/dimension/enum.rb +32 -0
- data/lib/active_reporter/dimension/number.rb +51 -0
- data/lib/active_reporter/dimension/time.rb +93 -0
- data/lib/active_reporter/evaluator.rb +2 -0
- data/lib/active_reporter/evaluator/base.rb +17 -0
- data/lib/active_reporter/evaluator/block.rb +15 -0
- data/lib/active_reporter/inflector.rb +8 -0
- data/lib/active_reporter/invalid_params_error.rb +4 -0
- data/lib/active_reporter/report.rb +102 -0
- data/lib/active_reporter/report/aggregation.rb +297 -0
- data/lib/active_reporter/report/definition.rb +195 -0
- data/lib/active_reporter/report/metrics.rb +75 -0
- data/lib/active_reporter/report/validation.rb +106 -0
- data/lib/active_reporter/serializer.rb +7 -0
- data/lib/active_reporter/serializer/base.rb +103 -0
- data/lib/active_reporter/serializer/csv.rb +22 -0
- data/lib/active_reporter/serializer/form_field.rb +134 -0
- data/lib/active_reporter/serializer/hash_table.rb +12 -0
- data/lib/active_reporter/serializer/highcharts.rb +200 -0
- data/lib/active_reporter/serializer/nested_hash.rb +11 -0
- data/lib/active_reporter/serializer/table.rb +21 -0
- data/lib/active_reporter/tracker.rb +2 -0
- data/lib/active_reporter/tracker/base.rb +15 -0
- data/lib/active_reporter/tracker/delta.rb +9 -0
- data/lib/active_reporter/version.rb +3 -0
- data/lib/tasks/active_reporter_tasks.rake +4 -0
- data/spec/acceptance/data_spec.rb +381 -0
- data/spec/active_reporter/aggregator_spec.rb +102 -0
- data/spec/active_reporter/dimension/base_spec.rb +102 -0
- data/spec/active_reporter/dimension/bin/set_spec.rb +83 -0
- data/spec/active_reporter/dimension/bin/table_spec.rb +47 -0
- data/spec/active_reporter/dimension/bin_spec.rb +77 -0
- data/spec/active_reporter/dimension/category_spec.rb +60 -0
- data/spec/active_reporter/dimension/enum_spec.rb +94 -0
- data/spec/active_reporter/dimension/number_spec.rb +71 -0
- data/spec/active_reporter/dimension/time_spec.rb +61 -0
- data/spec/active_reporter/report_spec.rb +597 -0
- data/spec/active_reporter/serializer/hash_table_spec.rb +45 -0
- data/spec/active_reporter/serializer/highcharts_spec.rb +113 -0
- data/spec/active_reporter/serializer/table_spec.rb +62 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +26 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/site_controller.rb +11 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/author.rb +4 -0
- data/spec/dummy/app/models/comment.rb +4 -0
- data/spec/dummy/app/models/data_builder.rb +112 -0
- data/spec/dummy/app/models/post.rb +6 -0
- data/spec/dummy/app/models/post_report.rb +14 -0
- data/spec/dummy/app/views/layouts/application.html.erb +17 -0
- data/spec/dummy/app/views/site/report.html.erb +73 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +57 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/migrate/20150714202319_add_dummy_models.rb +25 -0
- data/spec/dummy/db/schema.rb +43 -0
- data/spec/dummy/db/seeds.rb +1 -0
- data/spec/dummy/log/test.log +37033 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/factories.rb +29 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/float.rb +8 -0
- metadata +385 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'active_reporter/dimension/base'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
module Dimension
|
5
|
+
class Bin < Base
|
6
|
+
MAX_BINS = 2_000
|
7
|
+
|
8
|
+
def max_bins
|
9
|
+
self.class::MAX_BINS
|
10
|
+
end
|
11
|
+
|
12
|
+
def min
|
13
|
+
@min ||= filter_min || report.records.minimum(expression)
|
14
|
+
end
|
15
|
+
alias bin_start min
|
16
|
+
|
17
|
+
def max
|
18
|
+
@max ||= filter_max || report.records.maximum(expression)
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter_min
|
22
|
+
filter_values_for(:min).min
|
23
|
+
end
|
24
|
+
|
25
|
+
def filter_max
|
26
|
+
filter_values_for(:max).max
|
27
|
+
end
|
28
|
+
|
29
|
+
def domain
|
30
|
+
min.nil? || max.nil? ? 0 : (max - min)
|
31
|
+
end
|
32
|
+
|
33
|
+
def group_values
|
34
|
+
@group_values ||= to_bins(array_param(:bins).presence || autopopulate_bins)
|
35
|
+
end
|
36
|
+
|
37
|
+
def filter_values
|
38
|
+
@filter_values ||= to_bins(super)
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter(relation)
|
42
|
+
filter_values.filter(relation, expression)
|
43
|
+
end
|
44
|
+
|
45
|
+
def group(relation)
|
46
|
+
group_values.group(relation, expression, sql_value_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_params!
|
50
|
+
super
|
51
|
+
|
52
|
+
if params.key?(:bin_count)
|
53
|
+
invalid_param!(:bin_count, "must be numeric") unless ActiveReporter.numeric?(params[:bin_count])
|
54
|
+
invalid_param!(:bin_count, "must be greater than 0") unless params[:bin_count].to_i > 0
|
55
|
+
invalid_param!(:bin_count, "must be less than #{max_bins}") unless params[:bin_count].to_i <= max_bins
|
56
|
+
end
|
57
|
+
|
58
|
+
if array_param(:bins).present?
|
59
|
+
invalid_param!(:bins, "must be hashes with min/max keys and valid values, or nil") unless group_values.all?(&:valid?)
|
60
|
+
end
|
61
|
+
|
62
|
+
if array_param(:only).present?
|
63
|
+
invalid_param!(:only, "must be hashes with min/max keys and valid values, or nil") unless filter_values.all?(&:valid?)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def filter_values_for(key)
|
70
|
+
filter_values.map { |filter_value| filter_value.send(key) }.compact
|
71
|
+
end
|
72
|
+
|
73
|
+
def table
|
74
|
+
self.class.const_get(:Table)
|
75
|
+
end
|
76
|
+
|
77
|
+
def set
|
78
|
+
self.class.const_get(:Set)
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_bins(bins)
|
82
|
+
table.new(bins.map(&method(:to_bin)))
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_bin(bin)
|
86
|
+
set.from_hash(bin)
|
87
|
+
end
|
88
|
+
|
89
|
+
def sanitize_sql_value(value)
|
90
|
+
set.from_sql(value)
|
91
|
+
end
|
92
|
+
|
93
|
+
def data_contains_nil?
|
94
|
+
report.records.where("#{expression} IS NULL").exists?
|
95
|
+
end
|
96
|
+
|
97
|
+
def autopopulate_bins
|
98
|
+
return [] if bin_start.blank? || max.blank?
|
99
|
+
|
100
|
+
bin_max = filter_values_for(:max).present? ? (max - bin_width) : max
|
101
|
+
|
102
|
+
bin_count = (bin_max - bin_start)/(bin_width)
|
103
|
+
invalid_param!(:bin_width, "is too small for the domain; would generate #{bin_count} bins") if bin_count > max_bins
|
104
|
+
|
105
|
+
bin_edge = bin_start
|
106
|
+
bins = []
|
107
|
+
|
108
|
+
loop do
|
109
|
+
break if bin_edge > bin_max
|
110
|
+
|
111
|
+
bin = { min: bin_edge, max: bin_edge + bin_width }
|
112
|
+
bins << bin
|
113
|
+
bin_edge = bin[:max]
|
114
|
+
end
|
115
|
+
|
116
|
+
bins.reverse! if sort_desc?
|
117
|
+
( nulls_last? ? bins.push(nil) : bins.unshift(nil) ) if data_contains_nil?
|
118
|
+
|
119
|
+
bins
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module ActiveReporter
|
2
|
+
module Dimension
|
3
|
+
class Bin
|
4
|
+
class Set
|
5
|
+
class << self
|
6
|
+
def from_hash(source)
|
7
|
+
# Returns either a bin or nil, depending on whether the input is valid.
|
8
|
+
case source
|
9
|
+
when nil
|
10
|
+
new(nil, nil)
|
11
|
+
when Hash then
|
12
|
+
min, max = source.symbolize_keys.values_at(:min, :max)
|
13
|
+
new(min.presence, max.presence) unless min.blank? && max.blank?
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_sql(value)
|
20
|
+
case value
|
21
|
+
when /^([^,]+),(.+)$/ then new($1, $2)
|
22
|
+
when /^([^,]+),$/ then new($1, nil)
|
23
|
+
when /^,(.+)$/ then new(nil, $1)
|
24
|
+
when ',', nil then new(nil, nil)
|
25
|
+
else
|
26
|
+
raise "Unexpected SQL bin format #{value}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(min, max)
|
32
|
+
@min = min
|
33
|
+
@max = max
|
34
|
+
end
|
35
|
+
|
36
|
+
def min
|
37
|
+
@min && parse(@min)
|
38
|
+
end
|
39
|
+
|
40
|
+
def max
|
41
|
+
@max && parse(@max)
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
(@min.nil? || parses?(@min)) && (@max.nil? || parses?(@max))
|
46
|
+
end
|
47
|
+
|
48
|
+
def parses?(value)
|
49
|
+
parse(value).present? rescue false
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse(value)
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
def quote(value)
|
57
|
+
ActiveRecord::Base.connection.quote(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def cast(value)
|
61
|
+
quote(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def bin_text
|
65
|
+
"#{min},#{max}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def cast_bin_text
|
69
|
+
case ActiveReporter.database_type
|
70
|
+
when :postgres, :sqlite
|
71
|
+
"CAST(#{quote(bin_text)} AS text)"
|
72
|
+
else
|
73
|
+
quote(bin_text)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def row_sql
|
78
|
+
"SELECT #{cast(min)} AS min, #{cast(max)} AS max, #{cast_bin_text} AS bin_text"
|
79
|
+
end
|
80
|
+
|
81
|
+
def contains_sql(expr)
|
82
|
+
case bin_edges
|
83
|
+
when :min_and_max
|
84
|
+
"(#{expr} >= #{quote(min)} AND #{expr} < #{quote(max)})"
|
85
|
+
when :min
|
86
|
+
"#{expr} >= #{quote(min)}"
|
87
|
+
when :max
|
88
|
+
"#{expr} < #{quote(max)}"
|
89
|
+
else
|
90
|
+
"#{expr} IS NULL"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def as_json(*)
|
95
|
+
@as_json ||= case bin_edges
|
96
|
+
when :min_and_max
|
97
|
+
{ min: min, max: max }
|
98
|
+
when :min
|
99
|
+
{ min: min }
|
100
|
+
when :max
|
101
|
+
{ max: max }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def [](key)
|
106
|
+
case key.to_s
|
107
|
+
when 'min' then min
|
108
|
+
when 'max' then max
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def has_key?(key)
|
113
|
+
%w[min max].include?(key.to_s)
|
114
|
+
end
|
115
|
+
alias key? has_key?
|
116
|
+
|
117
|
+
def values_at(*keys)
|
118
|
+
keys.map { |k| self[key] }
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
"<Bin @min=#{min.inspect} @max=#{max.inspect}>"
|
123
|
+
end
|
124
|
+
|
125
|
+
def hash
|
126
|
+
as_json.hash
|
127
|
+
end
|
128
|
+
|
129
|
+
def ==(other)
|
130
|
+
if other.nil?
|
131
|
+
min.nil? && max.nil?
|
132
|
+
else
|
133
|
+
min == other[:min] && max == other[:max]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
alias eql? ==
|
137
|
+
|
138
|
+
def bin_edges
|
139
|
+
case
|
140
|
+
when min_and_max? then :min_and_max
|
141
|
+
when min? then :min
|
142
|
+
when max? then :max
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def min_and_max?
|
149
|
+
min.present? && max.present?
|
150
|
+
end
|
151
|
+
|
152
|
+
def min?
|
153
|
+
min.present?
|
154
|
+
end
|
155
|
+
|
156
|
+
def max?
|
157
|
+
max.present?
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveReporter
|
2
|
+
module Dimension
|
3
|
+
class Bin
|
4
|
+
class Table < Array
|
5
|
+
def initialize(values)
|
6
|
+
super(values.compact)
|
7
|
+
end
|
8
|
+
|
9
|
+
def filter(relation, expr)
|
10
|
+
relation.where(any_contain(expr))
|
11
|
+
end
|
12
|
+
|
13
|
+
def group(relation, expr, value_name)
|
14
|
+
name = "#{value_name}_bin_table"
|
15
|
+
|
16
|
+
bin_join = <<~SQL
|
17
|
+
INNER JOIN (
|
18
|
+
#{rows.join(" UNION\n ")}
|
19
|
+
) AS #{name} ON (
|
20
|
+
CASE
|
21
|
+
WHEN #{name}.min IS NULL AND #{name}.max IS NULL THEN (#{expr} IS NULL)
|
22
|
+
WHEN #{name}.min IS NULL THEN (#{expr} < #{name}.max)
|
23
|
+
WHEN #{name}.max IS NULL THEN (#{expr} >= #{name}.min)
|
24
|
+
ELSE ((#{expr} >= #{name}.min) AND (#{expr} < #{name}.max))
|
25
|
+
END
|
26
|
+
)
|
27
|
+
SQL
|
28
|
+
|
29
|
+
selection = "#{name}.bin_text AS #{value_name}"
|
30
|
+
relation.joins(bin_join).select(selection).group(value_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rows
|
34
|
+
map(&:row_sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
def any_contain(expr)
|
38
|
+
map { |bin| bin.contains_sql(expr) }.join(' OR ')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'active_reporter/dimension/base'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
module Dimension
|
5
|
+
class Category < Base
|
6
|
+
def filter(relation)
|
7
|
+
values = filter_values
|
8
|
+
query = "#{expression} IN (?)"
|
9
|
+
query = "#{expression} IS NULL OR #{query}" if values.include?(nil)
|
10
|
+
relation.where(query, values.compact)
|
11
|
+
end
|
12
|
+
|
13
|
+
def group(relation)
|
14
|
+
order relation.select("#{expression} AS #{sql_value_name}").group(sql_value_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def group_values
|
18
|
+
return filter_values if filtering?
|
19
|
+
|
20
|
+
i = report.groupers.index(self)
|
21
|
+
report.raw_data.keys.map { |x| x[i] }.uniq
|
22
|
+
end
|
23
|
+
|
24
|
+
def all_values
|
25
|
+
relate(report.base_relation).pluck("DISTINCT #{expression}").map(&method(:sanitize_sql_value))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'active_reporter/dimension/category'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
module Dimension
|
5
|
+
class Enum < Category
|
6
|
+
def group_values
|
7
|
+
return filter_values if filtering?
|
8
|
+
|
9
|
+
# i = report.groupers.key(self)
|
10
|
+
all_values & report.raw_data.keys.map { |x| x[0] }.uniq
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_values
|
14
|
+
enum_values.keys.unshift(nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def enum_values
|
20
|
+
model.defined_enums[attribute.to_s]
|
21
|
+
end
|
22
|
+
|
23
|
+
def sanitize_sql_value(value)
|
24
|
+
enum_values.invert[value]
|
25
|
+
end
|
26
|
+
|
27
|
+
def enum?
|
28
|
+
true # Hash(model&.defined_enums).include?(attribute.to_s)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_reporter/dimension/bin'
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
module Dimension
|
5
|
+
class Number < Bin
|
6
|
+
DEFAULT_BIN_COUNT = 10
|
7
|
+
|
8
|
+
def validate_params!
|
9
|
+
super
|
10
|
+
|
11
|
+
if params.key?(:bin_width)
|
12
|
+
invalid_param!(:bin_width, 'must be numeric') unless ActiveReporter.numeric?(params[:bin_width])
|
13
|
+
invalid_param!(:bin_width, 'must be greater than 0') unless params[:bin_width].to_f > 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def bin_width
|
18
|
+
case
|
19
|
+
when params.key?(:bin_width)
|
20
|
+
params[:bin_width].to_f
|
21
|
+
when domain.zero?
|
22
|
+
1
|
23
|
+
when params.key?(:bin_count)
|
24
|
+
domain / params[:bin_count].to_f
|
25
|
+
else
|
26
|
+
default_bin_width
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def default_bin_width
|
33
|
+
domain / default_bin_count.to_f
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_bin_count
|
37
|
+
self.class::DEFAULT_BIN_COUNT
|
38
|
+
end
|
39
|
+
|
40
|
+
class Set < Bin::Set
|
41
|
+
def parses?(value)
|
42
|
+
ActiveReporter.numeric?(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse(value)
|
46
|
+
value.to_f
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'active_reporter/inflector'
|
2
|
+
require 'active_reporter/dimension/bin'
|
3
|
+
|
4
|
+
module ActiveReporter
|
5
|
+
module Dimension
|
6
|
+
class Time < Bin
|
7
|
+
STEPS = %i(seconds minutes hours days weeks months years)
|
8
|
+
BIN_STEPS = (STEPS - [:seconds]).map { |step| step.to_s.singularize(:_gem_active_reporter) }
|
9
|
+
DURATION_PATTERN = /\A\d+ (?:#{STEPS.map{ |step| "#{step}?" }.join('|')})\z/
|
10
|
+
|
11
|
+
def validate_params!
|
12
|
+
super
|
13
|
+
|
14
|
+
invalid_param!(:bin_width, "must be a hash of one of #{STEPS} to an integer") if params.key?(:bin_width) && !valid_duration?(params[:bin_width])
|
15
|
+
end
|
16
|
+
|
17
|
+
def bin_width
|
18
|
+
@bin_width ||= case
|
19
|
+
when params.key?(:bin_width)
|
20
|
+
custom_bin_width
|
21
|
+
when params.key?(:bin_count) && domain > 0
|
22
|
+
(domain / params[:bin_count].to_f).seconds
|
23
|
+
else
|
24
|
+
default_bin_width
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def bin_start
|
29
|
+
# ensure that each autogenerated bin represents a correctly aligned
|
30
|
+
# day/week/month/year
|
31
|
+
bin_start = super
|
32
|
+
|
33
|
+
return if bin_start.nil?
|
34
|
+
|
35
|
+
step = BIN_STEPS.detect { |step| bin_width == 1.send(step) }
|
36
|
+
step.present? ? bin_start.send(:"beginning_of_#{step}") : bin_start
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def custom_bin_width
|
42
|
+
case params[:bin_width]
|
43
|
+
when Hash
|
44
|
+
params[:bin_width].map { |step, n| n.send(step) }.sum
|
45
|
+
when String
|
46
|
+
n, step = params[:bin_width].split.map(&:strip)
|
47
|
+
n.to_i.send(step)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid_duration?(d)
|
52
|
+
case d
|
53
|
+
when Hash
|
54
|
+
d.all? { |step, n| step.to_sym.in?(STEPS) && n.is_a?(Numeric) }
|
55
|
+
when String
|
56
|
+
d =~ DURATION_PATTERN
|
57
|
+
else
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_bin_width
|
63
|
+
case domain
|
64
|
+
when 0 then 1.day
|
65
|
+
when 0..1.minute then 1.second
|
66
|
+
when 0..2.hours then 1.minute
|
67
|
+
when 0..2.days then 1.hour
|
68
|
+
when 0..2.weeks then 1.day
|
69
|
+
when 0..2.months then 1.week
|
70
|
+
when 0..2.years then 1.month
|
71
|
+
else 1.year
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Set < Bin::Set
|
76
|
+
def parse(value)
|
77
|
+
::Time.zone.parse(value.to_s.gsub('"', ''))
|
78
|
+
end
|
79
|
+
|
80
|
+
def cast(value)
|
81
|
+
case ActiveReporter.database_type
|
82
|
+
when :postgres
|
83
|
+
"CAST(#{super} AS timestamp with time zone)"
|
84
|
+
when :sqlite
|
85
|
+
"DATETIME(#{super})"
|
86
|
+
else
|
87
|
+
"CAST(#{super} AS DATETIME)"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|