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