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