active_reporter 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
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,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveReporter::Dimension::Number do
4
+ def new_dimension(dimension_params = {}, report_params = {}, opts = {})
5
+ report_params[:dimensions] = { foo: dimension_params }
6
+ ActiveReporter::Dimension::Number.new(
7
+ :foo,
8
+ OpenStruct.new(params: report_params),
9
+ opts
10
+ )
11
+ end
12
+
13
+ def expect_error(&block)
14
+ expect { yield }.to raise_error(ActiveReporter::InvalidParamsError)
15
+ end
16
+
17
+ describe 'param validation' do
18
+ it 'yells unless :bin_width is numeric' do
19
+ expect_error { new_dimension(bin_width: '') }
20
+ expect_error { new_dimension(bin_width: '49er') }
21
+ expect_error { new_dimension(bin_width: { seconds: 1 }) }
22
+ expect(new_dimension(bin_width: 10.5).bin_width).to eq 10.5
23
+ expect(new_dimension(bin_width: '10').bin_width).to eq 10.0
24
+ end
25
+ end
26
+
27
+ describe '#bin_width' do
28
+ it 'reads from params' do
29
+ dimension = new_dimension(bin_width: 7)
30
+ expect(dimension.bin_width).to eq 7
31
+ end
32
+
33
+ it 'can divide the domain into :bin_count bins' do
34
+ dimension = new_dimension(bin_count: 5, only: { min: 0, max: 5 })
35
+ expect(dimension.bin_width).to eq 1
36
+ allow(dimension).to receive(:data_contains_nil?).and_return(false)
37
+ expect(dimension.group_values).to eq [
38
+ { min: 0, max: 1 },
39
+ { min: 1, max: 2 },
40
+ { min: 2, max: 3 },
41
+ { min: 3, max: 4 },
42
+ { min: 4, max: 5 }
43
+ ]
44
+ end
45
+
46
+ it 'can include nils if they are present in the data' do
47
+ dimension = new_dimension(bin_count: 3, only: { min: 0, max: 3 })
48
+ allow(dimension).to receive(:data_contains_nil?).and_return(true)
49
+ expect(dimension.group_values).to eq [
50
+ { min: nil, max: nil },
51
+ { min: 0, max: 1 },
52
+ { min: 1, max: 2 },
53
+ { min: 2, max: 3 }
54
+ ]
55
+
56
+ dimension = new_dimension(bin_count: 3, only: { min: 0, max: 3 }, nulls_last: true)
57
+ allow(dimension).to receive(:data_contains_nil?).and_return(true)
58
+ expect(dimension.group_values).to eq [
59
+ { min: 0, max: 1 },
60
+ { min: 1, max: 2 },
61
+ { min: 2, max: 3 },
62
+ { min: nil, max: nil }
63
+ ]
64
+ end
65
+
66
+ it 'defaults to 10 equal bins' do
67
+ dimension = new_dimension(only: { min: 0, max: 5 })
68
+ expect(dimension.bin_width).to eq 0.5
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveReporter::Dimension::Time do
4
+ def new_dimension(dimension_params = {}, report_params = {}, opts = {})
5
+ report_params[:dimensions] = { foo: dimension_params }
6
+ ActiveReporter::Dimension::Time.new(
7
+ :foo,
8
+ OpenStruct.new(params: report_params),
9
+ opts
10
+ )
11
+ end
12
+
13
+ def expect_error(&block)
14
+ expect { yield }.to raise_error(ActiveReporter::InvalidParamsError)
15
+ end
16
+
17
+ describe 'param validation' do
18
+ it 'yells unless :bin_width is a duration hash' do
19
+ expect_error { new_dimension(bin_width: '') }
20
+ expect_error { new_dimension(bin_width: 5) }
21
+ expect_error { new_dimension(bin_width: { seconds: 'hey' }) }
22
+ expect_error { new_dimension(bin_width: { seconds: 1, chickens: 0 }) }
23
+ new_dimension(bin_width: { seconds: 1, minutes: 2 })
24
+ new_dimension(bin_width: { weeks: 12, years: 7 })
25
+ end
26
+
27
+ it 'yells unless :bins and :only values are times' do
28
+ expect_error { new_dimension(bins: { min: 'hey' }) }
29
+ expect_error { new_dimension(only: { min: 'hey' }) }
30
+ expect_error { new_dimension(only: [{ min: '2015-01-01', max: '2015-01-10' }, { min: 'chicken' }]) }
31
+ new_dimension(bins: { min: '2015-01-01', max: '2015-01-10' })
32
+ new_dimension(only: { min: '2015-01-01', max: '2015-01-10' })
33
+ new_dimension(only: [nil, { min: '2015-01-01', max: '2015-01-10' }, { max: '2015-02-10' }])
34
+ end
35
+ end
36
+
37
+ describe '#bin_width' do
38
+ it 'can translate a duration hash into an ActiveSupport::Duration' do
39
+ dimension = new_dimension(bin_width: { seconds: 10, minutes: 1 })
40
+ expect(dimension.bin_width).to eq 70.seconds
41
+ dimension = new_dimension(bin_width: { days: 8, weeks: 1 })
42
+ expect(dimension.bin_width).to eq 15.days
43
+ end
44
+
45
+ it 'can divide the domain into :bin_count bins' do
46
+ dimension = new_dimension(bin_count: 10, only: [{ min: '2015-01-01' }, { max: '2015-01-11' }])
47
+ allow(dimension).to receive(:data_contains_nil?).and_return(false)
48
+ expect(dimension.bin_width).to eq 1.day
49
+ expect(dimension.group_values.map(&:min).map(&:day)).to eq (1..10).to_a
50
+ end
51
+
52
+ it 'defaults to a sensical, standard duration' do
53
+ dimension = new_dimension(only: [{ min: '2015-01-01' }, { max: '2015-01-02' }])
54
+ expect(dimension.bin_width).to eq 1.hour
55
+ dimension = new_dimension(only: [{ min: '2015-01-01' }, { max: '2015-01-11' }])
56
+ expect(dimension.bin_width).to eq 1.day
57
+ dimension = new_dimension(only: [{ min: '2015-01-01' }, { max: '2015-02-11' }])
58
+ expect(dimension.bin_width).to eq 1.week
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,597 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveReporter::Report do
4
+ let(:report_model) do
5
+ Class.new(ActiveReporter::Report) do
6
+ report_on :Post
7
+ count_aggregator :count
8
+ sum_aggregator :likes
9
+ number_dimension :likes
10
+ category_dimension :author, model: :author, attribute: :name, relation: ->(r) { r.joins(:author) }
11
+ time_dimension :created_at
12
+ ratio_calculator :likes_ratio, aggregator: :likes
13
+ delta_tracker :likes_delta, aggregator: :likes
14
+ end
15
+ end
16
+
17
+ let(:groupers) { nil }
18
+ let(:aggregators) { nil }
19
+ let(:dimensions) { nil }
20
+ let(:parent_report) { nil }
21
+ let(:parent_groupers) { nil }
22
+ let(:calculators) { nil }
23
+ let(:trackers) { nil }
24
+ let(:report) { report_model.new({groupers: groupers, aggregators: aggregators, dimensions: dimensions, parent_report: parent_report, parent_groupers: parent_groupers, calculators: calculators, trackers: trackers}.compact) }
25
+
26
+ let(:year) { 1.year.ago.year }
27
+
28
+ let(:jan_datetime) { Time.new(year,1,1,0,0,0,0) }
29
+ let(:feb_datetime) { Time.new(year,2,1,0,0,0,0) }
30
+ let(:mar_datetime) { Time.new(year,3,1,0,0,0,0) }
31
+ let(:apr_datetime) { Time.new(year,4,1,0,0,0,0) }
32
+
33
+ let(:jan) { { min: jan_datetime, max: jan_datetime.next_month } }
34
+ let(:feb) { { min: feb_datetime, max: feb_datetime.next_month } }
35
+ let(:mar) { { min: mar_datetime, max: mar_datetime.next_month } }
36
+ let(:apr) { { min: apr_datetime, max: apr_datetime.next_month } }
37
+
38
+ describe '.autoreport_on' do
39
+ let(:report_model) { Class.new(ActiveReporter::Report) { autoreport_on :Post } }
40
+
41
+ it 'infers dimensions from columns' do
42
+ expect(report_model.dimensions.keys).to include(*%i[created_at updated_at title author likes])
43
+ end
44
+
45
+ it "should properly store created_at dimension class" do
46
+ expect(report_model.dimensions[:created_at][:axis_class]).to eq ActiveReporter::Dimension::Time
47
+ end
48
+
49
+ it "should properly store updated_at dimension class" do
50
+ expect(report_model.dimensions[:updated_at][:axis_class]).to eq ActiveReporter::Dimension::Time
51
+ end
52
+
53
+ it "should properly store likes dimension class" do
54
+ expect(report_model.dimensions[:likes][:axis_class]).to eq ActiveReporter::Dimension::Number
55
+ end
56
+
57
+ it "should properly store title dimension class" do
58
+ expect(report_model.dimensions[:title][:axis_class]).to eq ActiveReporter::Dimension::Category
59
+ end
60
+
61
+ it "should properly store author dimension class" do
62
+ expect(report_model.dimensions[:author][:axis_class]).to eq ActiveReporter::Dimension::Category
63
+ end
64
+
65
+ context 'with expression' do
66
+ let!(:report_model) do
67
+ Class.new(ActiveReporter::Report) do
68
+ report_on :Post
69
+ count_aggregator :count
70
+ sum_aggregator :likes
71
+ number_dimension :likes
72
+ category_dimension :author, expression: 'authors.name', relation: ->(r) { r.joins(:author) }
73
+ time_dimension :created_at
74
+ ratio_calculator :likes_ratio, aggregator: :likes
75
+ delta_tracker :likes_delta, aggregator: :likes
76
+ end
77
+ end
78
+
79
+ it 'should properly store author expression' do
80
+ expect(report_model.dimensions[:author][:opts][:expression]).to eq 'authors.name'
81
+ end
82
+ end
83
+ end
84
+
85
+ describe 'data access' do
86
+ let(:groupers) { %w(author created_at) }
87
+ let(:dimensions) { { created_at: { bin_width: { months: 1 }, only: { min: Date.new(year,1,1).to_s }}} }
88
+
89
+ let(:author1) { 'Tammy' }
90
+ let(:author2) { 'Timmy' }
91
+
92
+ let!(:author1_dec18_post) { create(:post, author: author1, created_at: Date.new(year.pred,12,18), likes: 23) }
93
+ let!(:author1_jan01_post) { create(:post, author: author1, created_at: Date.new(year,1,1), likes: 7) }
94
+ let!(:author1_jan12_post) { create(:post, author: author1, created_at: Date.new(year,1,12), likes: 4) }
95
+ let!(:author1_mar08_post) { create(:post, author: author1, created_at: Date.new(year,3,8), likes: 11) }
96
+
97
+ let!(:author2_jan15_post) { create(:post, author: author2, created_at: Date.new(year,1,15), likes: 3) }
98
+ let!(:author2_feb27_post) { create(:post, author: author2, created_at: Date.new(year,2,27), likes: 24) }
99
+ let!(:author2_feb28_post) { create(:post, author: author2, created_at: Date.new(year,2,28), likes: 0) }
100
+ let!(:author2_mar01_post) { create(:post, author: author2, created_at: Date.new(year,3,1), likes: 19) }
101
+ let!(:author2_apr08_post) { create(:post, author: author2, created_at: Date.new(year,4,8), likes: 8) }
102
+
103
+ let(:author1_dec_posts) { [author1_dec18_post] }
104
+ let(:author1_jan_posts) { [author1_jan01_post, author1_jan12_post] }
105
+ let(:author1_feb_posts) { [] }
106
+ let(:author1_mar_posts) { [author1_mar08_post] }
107
+ let(:author1_apr_posts) { [] }
108
+
109
+ let(:author2_dec_posts) { [] }
110
+ let(:author2_jan_posts) { [author2_jan15_post] }
111
+ let(:author2_feb_posts) { [author2_feb27_post, author2_feb28_post] }
112
+ let(:author2_mar_posts) { [author2_mar01_post] }
113
+ let(:author2_apr_posts) { [author2_apr08_post] }
114
+
115
+ let(:author1_dec_count) { author1_dec_posts.count }
116
+ let(:author1_jan_count) { author1_jan_posts.count }
117
+ let(:author1_feb_count) { author1_feb_posts.count }
118
+ let(:author1_mar_count) { author1_mar_posts.count }
119
+ let(:author1_apr_count) { author1_apr_posts.count }
120
+
121
+ let(:author2_dec_count) { author2_dec_posts.count }
122
+ let(:author2_jan_count) { author2_jan_posts.count }
123
+ let(:author2_feb_count) { author2_feb_posts.count }
124
+ let(:author2_mar_count) { author2_mar_posts.count }
125
+ let(:author2_apr_count) { author2_apr_posts.count }
126
+
127
+ let(:author1_dec_likes) { author1_dec_posts.sum(&:likes) }
128
+ let(:author1_jan_likes) { author1_jan_posts.sum(&:likes) }
129
+ let(:author1_feb_likes) { author1_feb_posts.sum(&:likes) }
130
+ let(:author1_mar_likes) { author1_mar_posts.sum(&:likes) }
131
+ let(:author1_apr_likes) { author1_apr_posts.sum(&:likes) }
132
+
133
+ let(:author2_dec_likes) { author2_dec_posts.sum(&:likes) }
134
+ let(:author2_jan_likes) { author2_jan_posts.sum(&:likes) }
135
+ let(:author2_feb_likes) { author2_feb_posts.sum(&:likes) }
136
+ let(:author2_mar_likes) { author2_mar_posts.sum(&:likes) }
137
+ let(:author2_apr_likes) { author2_apr_posts.sum(&:likes) }
138
+
139
+ it 'should return raw_data' do
140
+ expect(report.raw_data).to eq(
141
+ [author1, jan, 'count'] => author1_jan_count,
142
+ [author1, jan, 'likes'] => author1_jan_likes,
143
+ [author1, mar, 'count'] => author1_mar_count,
144
+ [author1, mar, 'likes'] => author1_mar_likes,
145
+ [author2, jan, 'count'] => author2_jan_count,
146
+ [author2, jan, 'likes'] => author2_jan_likes,
147
+ [author2, feb, 'count'] => author2_feb_count,
148
+ [author2, feb, 'likes'] => author2_feb_likes,
149
+ [author2, mar, 'count'] => author2_mar_count,
150
+ [author2, mar, 'likes'] => author2_mar_likes,
151
+ [author2, apr, 'count'] => author2_apr_count,
152
+ [author2, apr, 'likes'] => author2_apr_likes,
153
+ )
154
+ end
155
+
156
+ it 'should return flat_data' do
157
+ expect(report.flat_data).to eq(
158
+ [author1, jan, 'count'] => author1_jan_count,
159
+ [author1, jan, 'likes'] => author1_jan_likes,
160
+ [author1, feb, 'count'] => author1_feb_count,
161
+ [author1, feb, 'likes'] => author1_feb_likes,
162
+ [author1, mar, 'count'] => author1_mar_count,
163
+ [author1, mar, 'likes'] => author1_mar_likes,
164
+ [author1, apr, 'count'] => author1_apr_count,
165
+ [author1, apr, 'likes'] => author1_apr_likes,
166
+ [author2, jan, 'count'] => author2_jan_count,
167
+ [author2, jan, 'likes'] => author2_jan_likes,
168
+ [author2, feb, 'count'] => author2_feb_count,
169
+ [author2, feb, 'likes'] => author2_feb_likes,
170
+ [author2, mar, 'count'] => author2_mar_count,
171
+ [author2, mar, 'likes'] => author2_mar_likes,
172
+ [author2, apr, 'count'] => author2_apr_count,
173
+ [author2, apr, 'likes'] => author2_apr_likes,
174
+ )
175
+ end
176
+
177
+ it 'should return nested_data' do
178
+ expect(report.nested_data).to eq [
179
+ { key: jan, values: [
180
+ { key: author1, values: [{ key: 'count', value: author1_jan_count }, { key: 'likes', value: author1_jan_likes }] },
181
+ { key: author2, values: [{ key: 'count', value: author2_jan_count }, { key: 'likes', value: author2_jan_likes }] },
182
+ ] },
183
+ { key: feb, values: [
184
+ { key: author1, values: [{ key: 'count', value: author1_feb_count }, { key: 'likes', value: author1_feb_likes }] },
185
+ { key: author2, values: [{ key: 'count', value: author2_feb_count }, { key: 'likes', value: author2_feb_likes }] },
186
+ ] },
187
+ { key: mar, values: [
188
+ { key: author1, values: [{ key: 'count', value: author1_mar_count }, { key: 'likes', value: author1_mar_likes }] },
189
+ { key: author2, values: [{ key: 'count', value: author2_mar_count }, { key: 'likes', value: author2_mar_likes }] },
190
+ ] },
191
+ { key: apr, values: [
192
+ { key: author1, values: [{ key: 'count', value: author1_apr_count }, { key: 'likes', value: author1_apr_likes }] },
193
+ { key: author2, values: [{ key: 'count', value: author2_apr_count }, { key: 'likes', value: author2_apr_likes }] },
194
+ ] }
195
+ ]
196
+ end
197
+
198
+ context 'with calculators' do
199
+ let(:parent_groupers) { %i(author) }
200
+ let(:parent_dimensions) { { created_at: { only: { min: Date.new(year,1,1).to_s }}} }
201
+ let(:aggregators) { %i(count likes) }
202
+ let(:parent_report) { report_model.new({groupers: parent_groupers, dimensions: parent_dimensions, aggregators: aggregators}) }
203
+ let(:calculators) { %i(likes_ratio) }
204
+
205
+ let(:author1_posts) { [author1_jan01_post, author1_jan12_post, author1_mar08_post] }
206
+ let(:author1_posts_likes) { author1_posts.sum(&:likes) }
207
+ let(:author2_posts) { [author2_jan15_post, author2_feb27_post, author2_feb28_post, author2_mar01_post, author2_apr08_post] }
208
+ let(:author2_posts_likes) { author2_posts.sum(&:likes) }
209
+
210
+ let(:author1_jan_likes_ratio) { author1_jan_posts.none? || author1_posts_likes.zero? ? nil : (author1_jan_likes/author1_posts_likes.to_f)*100 }
211
+ let(:author1_feb_likes_ratio) { author1_feb_posts.none? || author1_posts_likes.zero? ? nil : (author1_feb_likes/author1_posts_likes.to_f)*100 }
212
+ let(:author1_mar_likes_ratio) { author1_mar_posts.none? || author1_posts_likes.zero? ? nil : (author1_mar_likes/author1_posts_likes.to_f)*100 }
213
+ let(:author1_apr_likes_ratio) { author1_apr_posts.none? || author1_posts_likes.zero? ? nil : (author1_apr_likes/author1_posts_likes.to_f)*100 }
214
+
215
+ let(:author2_jan_likes_ratio) { author2_jan_posts.none? || author2_posts_likes.zero? ? nil : (author2_jan_likes/author2_posts_likes.to_f)*100 }
216
+ let(:author2_feb_likes_ratio) { author2_feb_posts.none? || author2_posts_likes.zero? ? nil : (author2_feb_likes/author2_posts_likes.to_f)*100 }
217
+ let(:author2_mar_likes_ratio) { author2_mar_posts.none? || author2_posts_likes.zero? ? nil : (author2_mar_likes/author2_posts_likes.to_f)*100 }
218
+ let(:author2_apr_likes_ratio) { author2_apr_posts.none? || author2_posts_likes.zero? ? nil : (author2_apr_likes/author2_posts_likes.to_f)*100 }
219
+
220
+ it 'should calculate' do
221
+ expect(report.data).to eq [
222
+ { key: jan, values: [
223
+ { key: author1, values: [
224
+ { key: 'count', value: author1_jan_count },
225
+ { key: 'likes', value: author1_jan_likes },
226
+ { key: 'likes_ratio', value: author1_jan_likes_ratio },
227
+ ] },
228
+ { key: author2, values: [
229
+ { key: 'count', value: author2_jan_count },
230
+ { key: 'likes', value: author2_jan_likes },
231
+ { key: 'likes_ratio', value: author2_jan_likes_ratio },
232
+ ] },
233
+ ] },
234
+ { key: feb, values: [
235
+ { key: author1, values: [
236
+ { key: 'count', value: author1_feb_count },
237
+ { key: 'likes', value: author1_feb_likes },
238
+ { key: 'likes_ratio', value: author1_feb_likes_ratio },
239
+ ] },
240
+ { key: author2, values: [
241
+ { key: 'count', value: author2_feb_count },
242
+ { key: 'likes', value: author2_feb_likes },
243
+ { key: 'likes_ratio', value: author2_feb_likes_ratio },
244
+ ] },
245
+ ] },
246
+ { key: mar, values: [
247
+ { key: author1, values: [
248
+ { key: 'count', value: author1_mar_count },
249
+ { key: 'likes', value: author1_mar_likes },
250
+ { key: 'likes_ratio', value: author1_mar_likes_ratio },
251
+ ] },
252
+ { key: author2, values: [
253
+ { key: 'count', value: author2_mar_count },
254
+ { key: 'likes', value: author2_mar_likes },
255
+ { key: 'likes_ratio', value: author2_mar_likes_ratio },
256
+ ] },
257
+ ]},
258
+ { key: apr, values: [
259
+ { key: author1, values: [
260
+ { key: 'count', value: author1_apr_count },
261
+ { key: 'likes', value: author1_apr_likes },
262
+ { key: 'likes_ratio', value: author1_apr_likes_ratio },
263
+ ] },
264
+ { key: author2, values: [
265
+ { key: 'count', value: author2_apr_count },
266
+ { key: 'likes', value: author2_apr_likes },
267
+ { key: 'likes_ratio', value: author2_apr_likes_ratio },
268
+ ] },
269
+ ]},
270
+ ]
271
+ end
272
+ end
273
+
274
+ context 'with trackers' do
275
+ let(:aggregators) { %i(count likes) }
276
+ let(:trackers) { %i(likes_delta) }
277
+
278
+ let(:author1_posts) { [author1_jan01_post, author1_jan12_post, author1_mar08_post] }
279
+ let(:author1_posts_likes) { author1_posts.sum(&:likes) }
280
+
281
+ let(:author2_posts) { [author2_jan15_post,author2_feb27_post, author2_feb28_post, author2_mar01_post, author2_apr08_post] }
282
+ let(:author2_posts_likes) { author2_posts.sum(&:likes) }
283
+
284
+ let(:author1_jan_likes_delta) { author1_dec_likes.zero? || author1_jan_likes.zero? ? nil : (author1_jan_likes/author1_dec_likes.to_f)*100 }
285
+ let(:author1_feb_likes_delta) { author1_jan_likes.zero? || author1_feb_likes.zero? ? nil : (author1_feb_likes/author1_jan_likes.to_f)*100 }
286
+ let(:author1_mar_likes_delta) { author1_feb_likes.zero? || author1_mar_likes.zero? ? nil : (author1_mar_likes/author1_feb_likes.to_f)*100 }
287
+ let(:author1_apr_likes_delta) { author1_mar_likes.zero? || author1_apr_likes.zero? ? nil : (author1_apr_likes/author1_mar_likes.to_f)*100 }
288
+
289
+ let(:author2_jan_likes_delta) { author2_dec_likes.zero? || author2_jan_likes.zero? ? nil : (author2_jan_likes/author2_dec_likes.to_f)*100 }
290
+ let(:author2_feb_likes_delta) { author2_jan_likes.zero? || author2_feb_likes.zero? ? nil : (author2_feb_likes/author2_jan_likes.to_f)*100 }
291
+ let(:author2_mar_likes_delta) { author2_feb_likes.zero? || author2_mar_likes.zero? ? nil : (author2_mar_likes/author2_feb_likes.to_f)*100 }
292
+ let(:author2_apr_likes_delta) { author2_mar_likes.zero? || author2_apr_likes.zero? ? nil : (author2_apr_likes/author2_mar_likes.to_f)*100 }
293
+
294
+ it 'should calculate' do
295
+ expect(report.data).to eq [
296
+ { key: jan, values: [
297
+ { key: author1, values: [
298
+ { key: 'count', value: author1_jan_count },
299
+ { key: 'likes', value: author1_jan_likes },
300
+ { key: 'likes_delta', value: author1_jan_likes_delta },
301
+ ] },
302
+ { key: author2, values: [
303
+ { key: 'count', value: author2_jan_count },
304
+ { key: 'likes', value: author2_jan_likes },
305
+ { key: 'likes_delta', value: author2_jan_likes_delta },
306
+ ] },
307
+ ] },
308
+ { key: feb, values: [
309
+ { key: author1, values: [
310
+ { key: 'count', value: author1_feb_count },
311
+ { key: 'likes', value: author1_feb_likes },
312
+ { key: 'likes_delta', value: author1_feb_likes_delta },
313
+ ] },
314
+ { key: author2, values: [
315
+ { key: 'count', value: author2_feb_count },
316
+ { key: 'likes', value: author2_feb_likes },
317
+ { key: 'likes_delta', value: author2_feb_likes_delta },
318
+ ] },
319
+ ] },
320
+ { key: mar, values: [
321
+ { key: author1, values: [
322
+ { key: 'count', value: author1_mar_count },
323
+ { key: 'likes', value: author1_mar_likes },
324
+ { key: 'likes_delta', value: author1_mar_likes_delta },
325
+ ] },
326
+ { key: author2, values: [
327
+ { key: 'count', value: author2_mar_count },
328
+ { key: 'likes', value: author2_mar_likes },
329
+ { key: 'likes_delta', value: author2_mar_likes_delta },
330
+ ] },
331
+ ]},
332
+ { key: apr, values: [
333
+ { key: author1, values: [
334
+ { key: 'count', value: author1_apr_count },
335
+ { key: 'likes', value: author1_apr_likes },
336
+ { key: 'likes_delta', value: author1_apr_likes_delta },
337
+ ] },
338
+ { key: author2, values: [
339
+ { key: 'count', value: author2_apr_count },
340
+ { key: 'likes', value: author2_apr_likes },
341
+ { key: 'likes_delta', value: author2_apr_likes_delta },
342
+ ] },
343
+ ]},
344
+ ]
345
+ end
346
+ end
347
+ end
348
+
349
+ describe '#dimensions' do
350
+ it 'is a curried hash' do
351
+ expect(report_model.dimensions.keys).to include(:likes, :author, :created_at)
352
+ expect(report.dimensions.keys).to include(:likes, :author, :created_at)
353
+ expect(report.dimensions[:likes]).to be_a ActiveReporter::Dimension::Number
354
+ expect(report.dimensions[:author]).to be_a ActiveReporter::Dimension::Category
355
+ expect(report.dimensions[:created_at]).to be_a ActiveReporter::Dimension::Time
356
+ end
357
+ end
358
+
359
+ describe '#calculators' do
360
+ let(:parent_groupers) { %i(author) }
361
+ let(:aggregators) { %i(count likes) }
362
+ let(:parent_report) { report_model.new({groupers: parent_groupers, aggregators: aggregators}) }
363
+ let(:calculators) { %i(likes_ratio) }
364
+
365
+ it 'should return configured calculators' do
366
+ expect(report.calculators).to include(:likes_ratio)
367
+ end
368
+ end
369
+
370
+ describe '#trackers' do
371
+ let(:parent_groupers) { %i(author) }
372
+ let(:aggregators) { %i(count likes) }
373
+ let(:parent_report) { report_model.new({groupers: parent_groupers, aggregators: aggregators}) }
374
+ let(:trackers) { %i(likes_delta) }
375
+
376
+ it 'should return configured trackers' do
377
+ expect(report.trackers).to include(:likes_delta)
378
+ end
379
+ end
380
+
381
+ describe '#params' do
382
+ let(:author1) { 'Phil' }
383
+ let(:author2) { 'Phyllis' }
384
+ let(:date) { Date.new(year,1,1) }
385
+ let(:author1_post1) { create(:post, author: author1, created_at: date) }
386
+ let(:author1_post2) { create(:post, author: author1, created_at: date) }
387
+ let(:author2_post1) { create(:post, author: author2, created_at: date) }
388
+ let(:author2_post2) { create(:post, author: author2, created_at: date) }
389
+
390
+ let(:all_posts) { [author1_post1, author1_post2, author2_post1, author2_post2] }
391
+ let(:author1_posts) { [author1_post1, author1_post2] }
392
+ let(:author2_posts) { [author2_post1, author2_post2] }
393
+
394
+ context 'where author dimension only allows empty string' do
395
+ let(:report) { report_model.new(dimensions: { author: { only: '' }}) }
396
+
397
+ it 'strips empty string but preserves nil by default' do
398
+ expect(report.params).to be_blank
399
+ expect(report.dimensions[:author].filter_values).to be_blank
400
+ expect(report.records).to contain_exactly(*all_posts)
401
+ end
402
+ end
403
+
404
+ context 'where author dimension only allows array of empty string' do
405
+ let(:report) { report_model.new(dimensions: { author: { only: [''] }}) }
406
+
407
+ it 'strips empty string but preserves nil by default' do
408
+ expect(report.params).to be_blank
409
+ expect(report.dimensions[:author].filter_values).to be_blank
410
+ expect(report.records).to contain_exactly(*all_posts)
411
+ end
412
+ end
413
+
414
+ context 'where author dimension only allows empty string or Phil' do
415
+ let(:report) { report_model.new(dimensions: { author: { only: ['', author1] }}) }
416
+
417
+ it 'strips empty string but preserves nil by default' do
418
+ expect(report.params).to be_present
419
+ expect(report.dimensions[:author].filter_values).to contain_exactly(author1)
420
+ expect(report.records).to contain_exactly(*author1_posts)
421
+ end
422
+ end
423
+
424
+ context 'where author dimension strips blank values and only allows empty string' do
425
+ let(:report) { report_model.new(strip_blanks: false, dimensions: { author: { only: '' }}) }
426
+
427
+ it 'strips empty string but preserves nil by default' do
428
+ expect(report.params).to be_present
429
+ expect(report.dimensions[:author].filter_values).to eq([''])
430
+ expect(report.records).to be_empty
431
+ end
432
+ end
433
+
434
+ context 'where author dimension only allows nil' do
435
+ let(:report) { report_model.new(dimensions: { author: { only: nil }}) }
436
+
437
+ it 'strips empty string but preserves nil by default' do
438
+ expect(report.params).to be_present
439
+ expect(report.dimensions[:author].filter_values).to eq [nil]
440
+ expect(report.records).to be_empty
441
+ end
442
+ end
443
+ end
444
+
445
+ describe '#parent_report' do
446
+ let(:groupers) { %i(author created_at) }
447
+ let(:aggregators) { %i(count likes) }
448
+ let(:dimensions) { { created_at: { bin_width: { months: 1 }}} }
449
+ let(:parent_report) { report_model.new({ groupers: %i(author), aggregators: aggregators }) }
450
+
451
+ it 'should return passed parent report' do
452
+ expect(report.parent_report).to be_a report_model
453
+ end
454
+ end
455
+
456
+ describe '#aggregators' do
457
+ it 'is a curried hash' do
458
+ expect(report_model.aggregators.keys).to eq [:count, :likes]
459
+ expect(report.aggregators.keys).to eq [:count, :likes]
460
+ expect(report.aggregators[:count]).to be_a ActiveReporter::Aggregator::Count
461
+ expect(report.aggregators[:likes]).to be_a ActiveReporter::Aggregator::Sum
462
+ end
463
+ end
464
+
465
+ describe '#groupers' do
466
+ it 'defaults to the first' do
467
+ expect(report.groupers).to eq [report.dimensions[:likes]]
468
+ end
469
+
470
+ context 'with created_at group' do
471
+ let(:groupers) { 'created_at' }
472
+
473
+ it 'can be set' do
474
+ expect(report.groupers).to eq [report.dimensions[:created_at]]
475
+ end
476
+ end
477
+
478
+ context 'with created_at and author groups' do
479
+ let(:groupers) { %w(created_at author) }
480
+
481
+ it 'can be set' do
482
+ expect(report.groupers).to eq [report.dimensions[:created_at], report.dimensions[:author]]
483
+ end
484
+ end
485
+
486
+ context 'with invalid group' do
487
+ let(:groupers) { %w(chickens) }
488
+
489
+ it 'should raise an exception' do
490
+ expect { report }.to raise_error(ActiveReporter::InvalidParamsError)
491
+ end
492
+ end
493
+
494
+ context 'on a report class with no dimensions declared' do
495
+ let(:report_model) do
496
+ Class.new(ActiveReporter::Report) do
497
+ report_on :Post
498
+ count_aggregator :count
499
+ end
500
+ end
501
+
502
+ specify 'there must be at least one defined' do
503
+ expect { report }.to raise_error Regexp.new('does not declare any dimensions')
504
+ end
505
+ end
506
+ end
507
+
508
+ describe '#aggregators' do
509
+ context 'where the report aggregators are set' do
510
+ let(:aggregators) { 'likes' }
511
+
512
+ it 'returns the set aggregators' do
513
+ expect(report.aggregators.values).to contain_exactly report.aggregators[:likes]
514
+ end
515
+ end
516
+
517
+ context 'where the report aggregators include an invalid value' do
518
+ let(:aggregators) { 'chicken' }
519
+
520
+ it 'should raise an exception' do
521
+ expect { report }.to raise_error(ActiveReporter::InvalidParamsError)
522
+ end
523
+ end
524
+
525
+ context 'on a report class with no dimensions declared' do
526
+ let(:report_model) do
527
+ Class.new(ActiveReporter::Report) do
528
+ report_on :Post
529
+ time_dimension :created_at
530
+ end
531
+ end
532
+
533
+ specify 'there must be at least one defined' do
534
+ expect { report }.to raise_error Regexp.new('does not declare any aggregators or trackers')
535
+ end
536
+ end
537
+ end
538
+
539
+ describe '#total_data' do
540
+ let(:groupers) { %w(author created_at) }
541
+ let(:aggregators) { %i(count likes) }
542
+ let(:dimensions) { { likes: { bin_width: 1 }, created_at: { bin_width: { months: 1 }}} }
543
+
544
+ let(:author1) { 'Timmy' }
545
+ let(:author2) { 'Tammy' }
546
+ let!(:author1_jan01_post) { create(:post, author: author1, created_at: Date.new(year,1,1), likes: 1) }
547
+ let!(:author1_jan12_post) { create(:post, author: author1, created_at: Date.new(year,1,12), likes: 2) }
548
+ let!(:author2_jan15_post) { create(:post, author: author2, created_at: Date.new(year,1,15), likes: 3) }
549
+ let!(:author2_mar01_post) { create(:post, author: author2, created_at: Date.new(year,3,1), likes: 4) }
550
+ let!(:author2_mar15_post) { create(:post, author: author2, created_at: Date.new(year,3,15), likes: 2) }
551
+
552
+ let(:all_posts) { [author1_jan01_post, author1_jan12_post, author2_jan15_post, author2_mar01_post, author2_mar15_post] }
553
+ let(:all_posts_count) { all_posts.count }
554
+ let(:all_posts_likes) { all_posts.sum(&:likes) }
555
+
556
+ it 'should return total_data' do
557
+ expect(report.total_data).to eq({
558
+ ['totals', 'count'] => all_posts_count,
559
+ ['totals', 'likes'] => all_posts_likes,
560
+ })
561
+ end
562
+
563
+ context 'with calculators' do
564
+ let(:parent_report_model) do
565
+ Class.new(ActiveReporter::Report) do
566
+ report_on :Post
567
+ count_aggregator :count
568
+ sum_aggregator :likes
569
+ max_aggregator :max_likes, attribute: :likes
570
+ number_dimension :likes
571
+ category_dimension :author, model: :author, attribute: :name, relation: ->(r) { r.joins(:author) }
572
+ time_dimension :created_at
573
+ end
574
+ end
575
+
576
+ let(:dimensions) { { likes: { bin_width: 1 }, created_at: { bin_width: { months: 1 }}, author: { only: author2 }} }
577
+ let(:parent_dimensions) { { likes: { bin_width: 1 }, created_at: { bin_width: { months: 1 }}} }
578
+ let(:parent_groupers) { %i(author) }
579
+ let(:calculators) { %i(likes_ratio) }
580
+ let(:trackers) { %i(likes_delta) }
581
+ let(:parent_report) { parent_report_model.new({groupers: parent_groupers, aggregators: aggregators, dimensions: parent_dimensions}) }
582
+
583
+ let(:author2_posts) { [author2_jan15_post, author2_mar01_post, author2_mar15_post] }
584
+ let(:author2_posts_count) { author2_posts.count }
585
+ let(:author2_posts_likes) { author2_posts.sum(&:likes) }
586
+ let(:author2_posts_likes_ratio) { all_posts_likes.zero? ? nil : (author2_posts_likes/all_posts_likes.to_f)*100 }
587
+
588
+ it 'should calculate' do
589
+ expect(report.total_data).to eq({
590
+ ['totals', 'count'] => author2_posts_count,
591
+ ['totals', 'likes'] => author2_posts_likes,
592
+ ['totals', 'likes_ratio'] => author2_posts_likes_ratio
593
+ })
594
+ end
595
+ end
596
+ end
597
+ end