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