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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cff7309f3eb428bd8bf2059d60d60158e49a1114b273cd646da5cfa666274598
4
+ data.tar.gz: ef9ce48d28afd9f7e64ab3302473367ab932f5f16a6ed28e613e42d746b46caa
5
+ SHA512:
6
+ metadata.gz: 1b366856d2b834fe2d27bfba9cede8018bfe1f9f5cfe3ef2464f4b65d9416a0014ba4007979924bc691dd2e1cd391ae970c03387f96365f5ac5572b4e4163c15
7
+ data.tar.gz: b87e54d19f354287f2fa16d3a8a693fc5216cdd8bae5d667092ac5c4676b132aa272ef6000d3b8dc31118a0856824c57a40e24e4488c3035935a4360d95fd1ad
@@ -0,0 +1,14 @@
1
+ Copyright 2015 Andrew Ross (https://github.com/asross/repor)
2
+ Copyright 2020 chaunce butterfield (https://github.com/chaunce/active_reporter)
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
5
+ (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
6
+ merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
12
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
13
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,436 @@
1
+ # ActiveReporter [![Actions Status](https://github.com/chaunce/active_reporter/workflows/Ruby/badge.svg)](https://github.com/chaunce/active_reporter/actions)
2
+
3
+ `ActiveReporter` is a framework for aggregating data about
4
+ [Rails](http://rubyonrails.org) models backed by
5
+ [PostgreSQL](http://www.postgresql.org), [MySQL](https://www.mysql.com), or
6
+ [SQLite](https://www.sqlite.org) databases. It's designed to be flexible
7
+ enough to accommodate many use cases, but opinionated enough to avoid the need
8
+ for boilerplate.
9
+
10
+ `ActiveReporter` is based on the `repor` gem by Andrew Ross https://github.com/asross/repor
11
+
12
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
13
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
14
+
15
+
16
+ - [Basic usage](#basic-usage)
17
+ - [Building reports](#building-reports)
18
+ - [Defining reports](#defining-reports)
19
+ - [Base relation](#base-relation)
20
+ - [Dimensions (x-axes)](#dimensions-x-axes)
21
+ - [Filtering by dimensions](#filtering-by-dimensions)
22
+ - [Grouping by dimensions](#grouping-by-dimensions)
23
+ - [Customizing dimensions](#customizing-dimensions)
24
+ - [Aggregators (y-axes)](#aggregators-y-axes)
25
+ - [Customizing aggregators](#customizing-aggregators)
26
+ - [Serializing reports](#serializing-reports)
27
+ - [Contributing](#contributing)
28
+ - [License](#license)
29
+
30
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
31
+
32
+ ## Basic usage
33
+
34
+ Here are some examples of how to define, run, and serialize a `ActiveReporter::Report`:
35
+
36
+ ```ruby
37
+ class PostReport < ActiveReporter::Report
38
+ report_on :Post
39
+
40
+ category_dimension :author, relation: ->(r) { r.joins(:author) },
41
+ expression: 'users.name'
42
+ number_dimension :likes
43
+ time_dimension :created_at
44
+
45
+ count_aggregator :number_of_posts
46
+ sum_aggregator :total_likes, expression: 'posts.likes'
47
+ array_aggregator :post_ids, expression: 'posts.id'
48
+ end
49
+
50
+ # show me # published posts from 2014-2015 with at least 4 likes, by author
51
+
52
+ report = PostReport.new(
53
+ relation: Post.published,
54
+ groupers: [:author],
55
+ aggregators: [:number_of_posts],
56
+ dimensions: {
57
+ likes: {
58
+ only: { min: 4 }
59
+ },
60
+ created_at: {
61
+ only: { min: '2014', max: '2015' }
62
+ }
63
+ }
64
+ )
65
+
66
+ puts report.data
67
+
68
+ # => [
69
+ # { key: 'James Joyce', value: 10 },
70
+ # { key: 'Margaret Atwood', value: 4 }
71
+ # { key: 'Toni Morrison', value: 5 }
72
+ # ]
73
+
74
+ # show me likes on specific authors' posts by author and year, from 1985-1987
75
+
76
+ report = PostReport.new(
77
+ groupers: [:author, :created_at],
78
+ aggregators: [:total_likes],
79
+ dimensions: {
80
+ created_at: {
81
+ only: { min: '1985', max: '1987' },
82
+ bin_width: 'year'
83
+ },
84
+ author: {
85
+ only: ['Edith Wharton', 'James Baldwin']
86
+ }
87
+ }
88
+ )
89
+
90
+ puts report.data
91
+
92
+ # => [{
93
+ # key: { min: Tue, 01 Jan 1985 00:00:00 UTC +00:00,
94
+ # max: Wed, 01 Jan 1986 00:00:00 UTC +00:00 },
95
+ # values: [
96
+ # { key: 'Edith Wharton', value: 35 },
97
+ # { key: 'James Baldwin', value: 13 }
98
+ # ]
99
+ # }, {
100
+ # key: { min: Wed, 01 Jan 1986 00:00:00 UTC +00:00,
101
+ # max: Thu, 01 Jan 1987 00:00:00 UTC +00:00 },
102
+ # values: [
103
+ # { key: 'Edith Wharton', value: 0 },
104
+ # { key: 'James Baldwin', value: 0 }
105
+ # ]
106
+ # }, {
107
+ # key: { min: Thu, 01 Jan 1987 00:00:00 UTC +00:00,
108
+ # max: Fri, 01 Jan 1988 00:00:00 UTC +00:00 },
109
+ # values: [
110
+ # { key: 'Edith Wharton', value: 0 },
111
+ # { key: 'James Baldwin', value: 19 }
112
+ # ]
113
+ # }]
114
+
115
+ csv_serializer = ActiveReporter::Serializer::Csv.new(report)
116
+ puts csv_serializer.csv_text
117
+
118
+ # => csv text string
119
+
120
+ chart_serializer = ActiveReporter::Serializer::Highcharts.new(report)
121
+ puts chart_serializer.highcharts_options
122
+
123
+ # => highcharts options hash
124
+ ```
125
+
126
+ To define a report, you declare dimensions (which represent attributes of your
127
+ data) and aggregators (which represent quantities you want to measure). To
128
+ run a report, you instantiate it with one aggregator and at least one dimension,
129
+ then inspect its `data`. You can also wrap it in a serializer to get results in
130
+ useful formats.
131
+
132
+ ## Building reports
133
+
134
+ Just call `ReportClass.new(params)`, where `params` is a hash with these keys:
135
+
136
+ - `aggregators` (required) is a list of the names of the aggregator(s) to aggregate by
137
+ - `groupers` (required) is a list of the names of the dimension(s) to group by
138
+ - `relation` (optional) provides an initial scope for the data
139
+ - `dimensions` (optional) holds dimension-specific filter or grouping options
140
+
141
+ See below for more details about dimension-specific parameters.
142
+
143
+ ## Defining reports
144
+
145
+ ### Base relation
146
+
147
+ A `ActiveReporter::Report` either needs to know what `ActiveRecord` class it is reporting
148
+ on, or it needs to know a `table_name` and a `base_relation`.
149
+
150
+ You can specify an `ActiveRecord` class by calling the `report_on` class method
151
+ with a class or class name, or if you prefer, you can override the other two as
152
+ instance methods.
153
+
154
+ By default, it will try to infer an `ActiveRecord` class from the report class
155
+ name by dropping `/Report$/` and constantizing.
156
+
157
+ ```ruby
158
+ class PostReport < ActiveReporter::Report
159
+ end
160
+
161
+ PostReport.new.table_name
162
+ # => 'posts'
163
+
164
+ PostReport.new.base_relation
165
+ # => Post.all
166
+
167
+ class PostStructuralReport < ActiveReporter::Report
168
+ report_on :Post
169
+
170
+ def base_relation
171
+ super.where(author: 'Foucault')
172
+ end
173
+ end
174
+
175
+ PostStructuralReport.new.table_name
176
+ # => 'posts'
177
+
178
+ PostStructuralReport.new.base_relation
179
+ # => Post.where(author: 'Foucault')
180
+ ```
181
+
182
+ Finally, you can also use `autoreport_on` if you'd like to automatically infer
183
+ dimensions from your columns and associations. `autoreport_on` will try to map
184
+ most columns to dimensions, and if the column in question is for a `belongs_to`
185
+ association, will even try to join and report on the association's name:
186
+
187
+ ```ruby
188
+ class PostReport < ActiveReporter::Report
189
+ autoreport_on Post
190
+ end
191
+
192
+ PostReport.new.dimensions.keys
193
+ # => %i[:created_at, :updated_at, :likes, :title, :author]
194
+
195
+ PostReport.new.dimensions[:author].expression
196
+ # => 'users.name'
197
+ ```
198
+
199
+ Autoreport behavior can be customized by overriding certain methods; see the
200
+ `ActiveReporter::Report` code for more information.
201
+
202
+ ### Dimensions (x-axes)
203
+
204
+ You define dimensions on your `ActiveReporter::Report` to represent attributes of your
205
+ data you're interested in. Dimensions objects can filter or group your relation
206
+ by a SQL expression, and accept/return simple Ruby values of various types.
207
+
208
+ There are several built-in types of dimensions:
209
+ - `Category`
210
+ - Groups/filters the relation by the discrete values of the `expression`
211
+ - `Number`
212
+ - Groups/filters the relation by binning a continuous numeric `expression`
213
+ - `Time`
214
+ - Like number dimensions, but the bins are increments of time
215
+
216
+ You define dimensions in your report class like this:
217
+
218
+ ```ruby
219
+ class PostReport < ActiveReporter::Report
220
+ category_dimension :status
221
+ number_dimension :author_rating, expression: 'users.rating',
222
+ relation: ->(r) { r.joins(:author) }
223
+ time_dimension :publication_date, expression: 'posts.published_at'
224
+ end
225
+ ```
226
+
227
+ The SQL expression a dimension uses defaults to:
228
+ ```ruby
229
+ "#{report.table_name}.#{dimension.name}"
230
+ ```
231
+
232
+ but this can be overridden by passing an `expression` option. Additionally, if
233
+ the filtering or grouping requires joins or other SQL operations, a custom
234
+ `relation` proc can be passed, which will be called beforehand.
235
+
236
+ #### Filtering by dimensions
237
+
238
+ All dimensions can be filtered to one or more values by passing in
239
+ `params[:dimensions][<dimension name>][:only]`.
240
+
241
+ `Category#only` should be passed the exact values you'd like to filter
242
+ to (or what will map to them after connection adapter quoting).
243
+
244
+ `Number` and `Time` are "bin" dimensions, and their `only`s
245
+ should be passed one or more bin ranges. Bin ranges should be hashes of at
246
+ least one of `min` and `max`, or they should just be `nil` to explicitly select
247
+ rows for which `expression` is null. Bin range filtering is `min`-inclusive but
248
+ `max`-exclusive. For `Number`, the bin values should be numbers or
249
+ strings of digits. For `Time`, the bin values should be dates/times or
250
+ `Time.zone.parse`-able strings.
251
+
252
+ #### Grouping by dimensions
253
+
254
+ To group by a dimension, pass its `name` to `params[:groupers]`.
255
+
256
+ For bin dimensions (`Number` and `Time`), where the values
257
+ being grouped by are ranges of numbers or times, you can specify additional
258
+ options to control the width and distribution of those bins. In particular,
259
+ you can pass values to:
260
+
261
+ - `params[:dimensions][<name>][:bins]`,
262
+ - `params[:dimensions][<name>][:bin_count]`, or
263
+ - `params[:dimensions][<name>][:bin_width]`
264
+
265
+ `bins` is the most general option; you can use it to divide the full domain of
266
+ the data into non-uniform, overlapping, and even null bin ranges. It should be
267
+ passed an array of the same min/max hashes or `nil` used in filtering.
268
+
269
+ `bin_count` will divide the domain of the data into a fixed number of bins. It
270
+ should be passed a positive integer.
271
+
272
+ `bin_width` will tile the domain with bins of a fixed width. It should be
273
+ passed a positive number for `Number`s and a "duration" for
274
+ `Time`s. Durations can either be strings of a number followed by a time
275
+ increment (minutes, hours, days, weeks, months, years), or they can be hashes
276
+ suitable for use with
277
+ [`ActiveSupport::TimeWithZone#advance`](http://apidock.com/rails/ActiveSupport/TimeWithZone/advance).
278
+ E.g.:
279
+
280
+ ```
281
+ params[:dimensions][<time dimension>][:bin_width] = '1 month'
282
+ params[:dimensions][<time dimension>][:bin_width] = { months: 2, hours: 2 }
283
+ ```
284
+
285
+ `Number`s will default to using 10 bins and `Time`s will
286
+ default to using a sensical increment of time given the domain; you can
287
+ customize this by overriding methods in those classes.
288
+
289
+ Note that when you inspect `report.data` after grouping by a bin dimension, you
290
+ will see the dimension values are actually `ActiveReporter::Bin::Base` objects,
291
+ which respond to `min`, `max`, and various json/Hash methods. These are meant
292
+ to provide a common interface for the different types of bins (double-bounded,
293
+ unbounded on one side, null) and handle mapping between SQL and Ruby
294
+ representations of their values. You may find bin objects useful in working
295
+ with report data, and they can also be customized.
296
+
297
+ If you want to change how `ActiveReporter` maps SQL values to the dimension values of
298
+ `report.data`, you can override `YourDimension#sanitize_sql_value`.
299
+
300
+ #### Customizing dimensions
301
+
302
+ You can define custom dimension classes by inheriting from one of the existing
303
+ ones:
304
+ ```ruby
305
+ class CaseInsensitiveCategoryDimension < ActiveReporter::Dimension::Category
306
+ def order_expression
307
+ "UPPER(#{super})"
308
+ end
309
+ end
310
+ ```
311
+
312
+ You can then use it in the definition of a report class like this:
313
+ ```ruby
314
+ class UserReport < ActiveReporter::Report
315
+ dimension :last_name, CaseInsensitiveCategoryDimension
316
+ end
317
+ ```
318
+
319
+ Common methods to override include `order_expression`, `sanitize_sql_value`,
320
+ `validate_params!`, `group_values`, and `default_bin_width`.
321
+
322
+ Note that if you inherit directly from `ActiveReporter::Dimension::Base`, you
323
+ will need to implement (at a minimum) `filter(relation)`, `group(relation)`, and
324
+ `group_values`. See the base dimension class for more details.
325
+
326
+ If you want custom behavior for bins, you can define `Set` and `Table`
327
+ classes nested inside your custom dimension classes (or override methods
328
+ directly on `ActiveReporter::Dimension::Bin::Set(Table)`,
329
+ `ActiveReporter::Dimension::Time::Set(Table)`, etc). See the relevant classes for more
330
+ details.
331
+
332
+ ### Aggregators (y-axes)
333
+
334
+ Aggregators take your groups and reduce them down to a single value. They
335
+ represent the quantities you're looking to measure across your dimensions.
336
+
337
+ There are several built-in types of aggregators:
338
+
339
+ - `Aggregator::Count`
340
+ - counts the number of distinct records in each group
341
+ - `Aggregator::Sum`
342
+ - sums an `expression` over each distinct record in each group
343
+ - `Aggregator::Average`
344
+ - sum divided by count
345
+ - `Aggregator::Min`
346
+ - finds the minimum value of `expression` in each group
347
+ - `Aggregator::Max`
348
+ - finds the maximum value of `expression` in each group
349
+ - `Aggregator::Array`
350
+ - returns an array of `expression` values in each group (PostgreSQL only)
351
+ - useful if you want to drill down into the data behind an aggregation
352
+
353
+ #### Customizing aggregators
354
+
355
+ By default, the `expression` will default to the aggregator name, but you can
356
+ achieve some level of customization by passing in `expression` or `relation`:
357
+
358
+ ```ruby
359
+ max_aggregator :max_likes, expression: 'posts.likes'
360
+
361
+ sum_aggregator :total_cost,
362
+ expression: 'invoices.hours_worked * invoices.hourly_rate'
363
+
364
+ avg_aggregator :mean_author_age, expression: 'AGE(users.dob)',
365
+ relation: ->(r) { r.joins(:author) }
366
+ ```
367
+
368
+ You can also define your own aggregator type if none of the existing ones meet
369
+ your needs:
370
+
371
+ ```ruby
372
+ class LengthAggregator < ActiveReporter::Aggregators::BaseAggregator
373
+ def aggregate(grouped_relation)
374
+ # check out the other aggregators for examples of what to do here.
375
+ end
376
+ end
377
+
378
+ # then:
379
+ aggregator :name_length, LengthAggregator, expression: 'posts.name'
380
+ ```
381
+
382
+ ## Serializing reports
383
+
384
+ After defining and running a report, you can wrap it in a serializer to get its
385
+ data in a more useful format.
386
+
387
+ `Serializer::Table` defines `caption`, `headers`, and `each_row`, which can be
388
+ used to construct a table. It also wraps dimension and aggregator names and
389
+ values in formatting methods, which can be overridden, e.g. if you would like to
390
+ use I18n for date or enum column formatting. You can override these methods on
391
+ `Serializer::Base` if you would like them to apply everywhere.
392
+
393
+ `Serializer::Csv` dumps the data from `Serializer::Table` to a CSV string or file.
394
+
395
+ `Serializer::Highcharts` can map reports with 1-3 grouping dimensions to options
396
+ for passing into the Highcharts charting library. Extra options included with
397
+ the raw data makes it easy to implement features like detailed tooltips and
398
+ drilldown.
399
+
400
+ `Serializer::FormField` represents report parameters as HTML form fields. Likely
401
+ you will want to implement your own form logic specific to your report class
402
+ and application design, but it provides an easy and somewhat extensible way to
403
+ get up and running.
404
+
405
+ See the serializer class files for more documentation.
406
+
407
+ ## Contributing
408
+
409
+ If you have suggestions for how to make any part of this library better, or if
410
+ you want to contribute extra dimensions, aggregators, serializers, please
411
+ submit them in a pull request (with test coverage).
412
+
413
+ To work on developing `ActiveReporter`, you will need to have Ruby and PostgreSQL,
414
+ MySQL, or SQLite3 installed. Then clone the repository and run:
415
+ ```sh
416
+ bundle install
417
+ cd spec/dummy
418
+ DB=<your db type> bundle exec rake db:create db:schema:load db:test:prepare
419
+ cd ../..
420
+ DB=<your db type> bundle exec rspec
421
+ ```
422
+
423
+ which will run the test suite. The options for `DB` are `sqlite`, `mysql`, and
424
+ `postgres` (the default). Preferably you should run it against all three, but
425
+ CI will also do so.
426
+
427
+ To see the dummy application in development mode, you can run:
428
+ ```sh
429
+ cd spec/dummy
430
+ DB=<your db type> bundle exec rake db:setup
431
+ DB=<your db type> bundle exec rails server
432
+ ```
433
+
434
+ ## License
435
+
436
+ [MIT](http://opensource.org/licenses/MIT)