active_reporting 0.2.0 → 0.5.0
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.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -2
- data/.travis.yml +13 -12
- data/CHANGELOG.md +56 -2
- data/Gemfile +6 -7
- data/README.md +57 -17
- data/Rakefile +2 -0
- data/active_reporting.gemspec +9 -8
- data/bin/console +1 -0
- data/lib/active_reporting.rb +5 -3
- data/lib/active_reporting/active_record_adaptor.rb +3 -1
- data/lib/active_reporting/configuration.rb +2 -0
- data/lib/active_reporting/dimension.rb +13 -3
- data/lib/active_reporting/dimension_filter.rb +3 -1
- data/lib/active_reporting/fact_model.rb +7 -3
- data/lib/active_reporting/metric.rb +3 -1
- data/lib/active_reporting/report.rb +3 -1
- data/lib/active_reporting/reporting_dimension.rb +85 -23
- data/lib/active_reporting/version.rb +3 -1
- metadata +24 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1bbb1cca5da777e98db50e52e0dd97a44e20cd9844c8d4d88cbb739485a7f22f
|
|
4
|
+
data.tar.gz: f3d5fcc3f6e174e6eb6ca582c57add3b844cc0deb632e6f844ee9990db31c93f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b57b0e82ebb5d1d645383d3aa88551b43c999c1443d0ecac6544d83295d6d1745f2956696499a5ce3ff249f8d91251e298a77e3921a68722d6842ffb7e90e705
|
|
7
|
+
data.tar.gz: 3fcec68b2de1a822ecef745f7b75ca6aaa7ae9587d7a25ce5ae5234a26e1d083bab01da4ccd70bafd47a9382810fb0932cb287ca7b18d20916789f4e0809b045
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
Exclude:
|
|
3
3
|
- test/**/*
|
|
4
|
-
|
|
4
|
+
- Gemfile
|
|
5
|
+
TargetRubyVersion: 2.3
|
|
6
|
+
|
|
7
|
+
Metrics/ClassLength:
|
|
8
|
+
Max: 150
|
|
5
9
|
|
|
6
10
|
Metrics/LineLength:
|
|
7
11
|
Max: 120
|
|
@@ -18,7 +22,7 @@ Style/ClassAndModuleChildren:
|
|
|
18
22
|
Style/Documentation:
|
|
19
23
|
Enabled: false
|
|
20
24
|
|
|
21
|
-
Style/
|
|
25
|
+
Style/Name:
|
|
22
26
|
Enabled: false
|
|
23
27
|
|
|
24
28
|
Style/RaiseArgs:
|
data/.travis.yml
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
sudo: false
|
|
2
2
|
language: ruby
|
|
3
|
+
services:
|
|
4
|
+
- mysql
|
|
5
|
+
addons:
|
|
6
|
+
postgresql: "9.6"
|
|
3
7
|
rvm:
|
|
4
|
-
- 2.
|
|
5
|
-
- 2.
|
|
6
|
-
- 2.
|
|
8
|
+
- 2.5.8
|
|
9
|
+
- 2.6.6
|
|
10
|
+
- 2.7.1
|
|
7
11
|
env:
|
|
8
|
-
- RAILS=
|
|
9
|
-
- RAILS=
|
|
10
|
-
- RAILS=
|
|
11
|
-
- RAILS=5-
|
|
12
|
-
- RAILS=5-
|
|
13
|
-
- RAILS=5-
|
|
14
|
-
- RAILS=4-2 DB=sqlite
|
|
15
|
-
- RAILS=4-2 DB=pg
|
|
16
|
-
- RAILS=4-2 DB=mysql
|
|
12
|
+
- RAILS=6-0 DB=sqlite
|
|
13
|
+
- RAILS=6-0 DB=pg
|
|
14
|
+
- RAILS=6-0 DB=mysql
|
|
15
|
+
- RAILS=5-2 DB=sqlite
|
|
16
|
+
- RAILS=5-2 DB=pg
|
|
17
|
+
- RAILS=5-2 DB=mysql
|
|
17
18
|
before_script:
|
|
18
19
|
- psql -c 'create database active_reporting_test;' -U postgres
|
|
19
20
|
- mysql -e 'create database active_reporting_test collate utf8_general_ci;'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
## 0.5.0 (2020-06-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* Fix Missing quotation marks in column names causing SLQ errors on MYSQL - *germanotm*
|
|
6
|
+
|
|
7
|
+
### Misc
|
|
8
|
+
|
|
9
|
+
* Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
|
|
10
|
+
|
|
11
|
+
## 0.4.2 (2019-11-01)
|
|
12
|
+
|
|
13
|
+
### Misc
|
|
14
|
+
|
|
15
|
+
* Test against Rails 6.0 final
|
|
16
|
+
* Fixed deprecated call to `to_hash` - *joshforbes*
|
|
17
|
+
* Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
|
|
18
|
+
|
|
19
|
+
## 0.4.1 (2019-05-28)
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* Hierarchical dimensions may now have custom keys in result (#16) - *andresgutgon*
|
|
24
|
+
|
|
25
|
+
### Misc
|
|
26
|
+
|
|
27
|
+
* Test against Raisl 6.0RC
|
|
28
|
+
* Loosen AR requirements. The gem will install for any AR version, but only ones listed in the README are supported
|
|
29
|
+
* Test against active Rubies
|
|
30
|
+
|
|
31
|
+
## 0.4.0 (2018-05-02)
|
|
32
|
+
|
|
33
|
+
### Breaking Changes
|
|
34
|
+
|
|
35
|
+
* Gemspec now requires Ruby 2.3 and later to install
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
* Dimension off of `datetime` columns by date parts in PostgreSQL (See README for details) (#10) - *niborg*
|
|
40
|
+
|
|
41
|
+
## 0.3.0 (2018-04-12)
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
* Specify rescue from LoadError for ransack (#9) - *niborg*
|
|
46
|
+
* Fix ransack fallback logic (#8) - *germanotm*
|
|
47
|
+
|
|
48
|
+
### Misc
|
|
49
|
+
|
|
50
|
+
* Test against Rails 5.2
|
|
51
|
+
* Test against Ruby 2.5
|
|
52
|
+
* Drop support for Rails 5.0 (EOL-ed)
|
|
53
|
+
* Drop support for Ruby 2.2 (EOL-ed)
|
|
54
|
+
|
|
1
55
|
## 0.2.0 (2017-06-17)
|
|
2
56
|
|
|
3
57
|
### Breaking Changes
|
|
@@ -6,11 +60,11 @@
|
|
|
6
60
|
|
|
7
61
|
### Bug Fixes
|
|
8
62
|
|
|
9
|
-
* `metric` lives on fact model and not metric (#3) - *
|
|
63
|
+
* `metric` lives on fact model and not metric (#3) - *wheeyls*
|
|
10
64
|
|
|
11
65
|
### Misc
|
|
12
66
|
|
|
13
|
-
* Readme corrections and updates (#2) - *
|
|
67
|
+
* Readme corrections and updates (#2) - *wheeyls*
|
|
14
68
|
|
|
15
69
|
## 0.1.1 (2017-04-22)
|
|
16
70
|
|
data/Gemfile
CHANGED
|
@@ -4,13 +4,12 @@ gemspec
|
|
|
4
4
|
|
|
5
5
|
gem 'simplecov', require: false
|
|
6
6
|
|
|
7
|
-
rails = ENV['RAILS'] || '5-
|
|
7
|
+
rails = ENV['RAILS'] || '5-2'
|
|
8
|
+
db = ENV['DB'] || 'sqlite'
|
|
8
9
|
|
|
9
10
|
case rails
|
|
10
|
-
when '
|
|
11
|
-
gem 'activerecord', '~>
|
|
12
|
-
when '
|
|
13
|
-
gem 'activerecord', '
|
|
14
|
-
else
|
|
15
|
-
gem 'activerecord', '~> 5.1.0'
|
|
11
|
+
when '5-2'
|
|
12
|
+
gem 'activerecord', '~> 5.2.0'
|
|
13
|
+
when '6-0'
|
|
14
|
+
gem 'activerecord', '6.0.0'
|
|
16
15
|
end
|
data/README.md
CHANGED
|
@@ -8,6 +8,10 @@ ActiveReporting implements various terminology used in Relational Online Analyti
|
|
|
8
8
|
|
|
9
9
|
ActiveReporting officially supports MySQL, PostgreSQL, and SQLite.
|
|
10
10
|
|
|
11
|
+
ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
|
|
12
|
+
|
|
13
|
+
ActiveReporting officially supports Rails 5.2, and 6.0. Other versions may work, but are not supported.
|
|
14
|
+
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
13
17
|
Add this line to your application's Gemfile:
|
|
@@ -45,8 +49,8 @@ Rails: ActiveRecord model
|
|
|
45
49
|
A dimension is a point of data used to "slice and dice" data from a fact model. It's either a column that lives on the fact table or a foreign key to another table.
|
|
46
50
|
|
|
47
51
|
Examples:
|
|
48
|
-
* A sales rep on a fact table of
|
|
49
|
-
* A state of an
|
|
52
|
+
* A sales rep on a fact table of sales
|
|
53
|
+
* A state of an sale on a state machine
|
|
50
54
|
* The manufacture on a fact table of widgets
|
|
51
55
|
|
|
52
56
|
SQL Equivalent: JOIN, GROUP BY
|
|
@@ -174,7 +178,7 @@ end
|
|
|
174
178
|
ActiveReporting assumes the column of a fact model used for summing, averaging, etc. is called `value`. This may be changed on a fact model using `measure=`. You may pass in a string or symbol of the column you wish to use for aggregations.
|
|
175
179
|
|
|
176
180
|
```ruby
|
|
177
|
-
class
|
|
181
|
+
class SaleFactModel < ActiveReporting::FactModel
|
|
178
182
|
self.measure = :total
|
|
179
183
|
end
|
|
180
184
|
```
|
|
@@ -215,6 +219,24 @@ class PhoneFactModel < ActiveReporting::FactModel
|
|
|
215
219
|
end
|
|
216
220
|
```
|
|
217
221
|
|
|
222
|
+
### Implicit hierarchies with datetime columns (PostgreSQL support only)
|
|
223
|
+
|
|
224
|
+
The fastest approach to group by certain date metrics is to create so-called "date dimensions". For
|
|
225
|
+
those Postgres users that are restricted from organizing their data in this way, Postgres provides
|
|
226
|
+
a way to group by `datetime` column data on the fly using the `date_trunc` function.
|
|
227
|
+
|
|
228
|
+
To use, declare a datetime dimension on a fact model as normal:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
class UserFactModel < ActiveReporting::FactModel
|
|
232
|
+
dimension :created_at
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
When creating a metric, ActiveReporting will recognize implicit hierarchies for this dimension. The hierarchies correspond to the [values](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC) supported by PostgreSQL. (See example under the metric section, below.)
|
|
237
|
+
|
|
238
|
+
*NOTE*: PRs welcomed to support this functionality in other databases.
|
|
239
|
+
|
|
218
240
|
## Configuring Dimension Filters
|
|
219
241
|
|
|
220
242
|
A dimension filter provides filtering for a report. In SQL-land, this is the `WHERE` clause.
|
|
@@ -251,8 +273,8 @@ A `Metric` is the basic building block used to describe a question you want to a
|
|
|
251
273
|
|
|
252
274
|
```ruby
|
|
253
275
|
my_metric = ActiveReporting::Metric.new(
|
|
254
|
-
:
|
|
255
|
-
fact_model:
|
|
276
|
+
:sale_total,
|
|
277
|
+
fact_model: SaleFactModel,
|
|
256
278
|
aggregate: :sum
|
|
257
279
|
)
|
|
258
280
|
```
|
|
@@ -263,7 +285,7 @@ my_metric = ActiveReporting::Metric.new(
|
|
|
263
285
|
|
|
264
286
|
`aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
|
|
265
287
|
|
|
266
|
-
`dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension. You may specify a hierarchy level by using a hash. (Examples: `[:sales_rep, {
|
|
288
|
+
`dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension. You may specify a hierarchy level by using a hash. (Examples: `[:sales_rep, {sale_date: :month}]`). In hierarchies you can customize the label in this way: `[{sale_date: { field: :month, name: :a_custom_name_for_month }}]`. If you use a hash instead of a Symbol to define a hierarchy the `field` item must be a valid field in your table. The `name` can be whatever label you want.
|
|
267
289
|
|
|
268
290
|
`dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
|
|
269
291
|
|
|
@@ -271,26 +293,40 @@ my_metric = ActiveReporting::Metric.new(
|
|
|
271
293
|
|
|
272
294
|
`order_by_dimension` - Allows you to set the ordering of the results based on a dimension label. (Examples: `{author: :desc}`, `{sales_ref: :asc}`)
|
|
273
295
|
|
|
296
|
+
For those using Postgres, you can take advantage of implicit hierarchies in `datetime` columns, as mentioned above:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class UserFactModel < ActiveReporting::FactModel
|
|
300
|
+
dimension :created_at
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
my_metric = ActiveReporting::Metric.new(
|
|
304
|
+
:my_total,
|
|
305
|
+
fact_model: UserFactModel,
|
|
306
|
+
dimensions: [{ created_at: :quarter } ]
|
|
307
|
+
)
|
|
308
|
+
```
|
|
309
|
+
|
|
274
310
|
## ActiveReporting::Report
|
|
275
311
|
|
|
276
312
|
A `Report` takes an `ActiveReporting::Metric` and ties everything together. It is responsible for building and executing the query to generate a result. The result is an simple array of hashing.
|
|
277
313
|
|
|
278
314
|
```ruby
|
|
279
315
|
metric = ActiveReporting::Metric.new(
|
|
280
|
-
:
|
|
281
|
-
fact_model:
|
|
282
|
-
|
|
316
|
+
:sale_count,
|
|
317
|
+
fact_model: SaleFactModel,
|
|
318
|
+
dimensions: [:sales_rep],
|
|
283
319
|
dimension_filter: {months_ago: 1}
|
|
284
320
|
)
|
|
285
321
|
|
|
286
322
|
report = ActiveReporting::Report.new(metric)
|
|
287
323
|
report.run
|
|
288
|
-
=> [{
|
|
324
|
+
=> [{sale_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{sale_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
|
|
289
325
|
```
|
|
290
326
|
|
|
291
327
|
A `Report` may also take additional arguments to merge with the `Metric`'s information. This can be user input for additional filters, or to expand on a base `Metric`.
|
|
292
328
|
|
|
293
|
-
`dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of
|
|
329
|
+
`dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of sales dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
|
|
294
330
|
|
|
295
331
|
`dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
|
|
296
332
|
|
|
@@ -300,15 +336,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor
|
|
|
300
336
|
|
|
301
337
|
```ruby
|
|
302
338
|
metric = ActiveReporting::Metric.new(
|
|
303
|
-
:
|
|
304
|
-
fact_model:
|
|
305
|
-
|
|
339
|
+
:sale_count,
|
|
340
|
+
fact_model: SaleFactModel,
|
|
341
|
+
dimensions: [:sales_rep],
|
|
306
342
|
dimension_filter: {months_ago: 1}
|
|
307
343
|
)
|
|
308
344
|
|
|
309
|
-
report = ActiveReporting.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
|
|
345
|
+
report = ActiveReporting::Report.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
|
|
310
346
|
report.run
|
|
311
|
-
=> [{
|
|
347
|
+
=> [{sale_count: 17, sales_rep: 'Mary Sue'}]
|
|
312
348
|
```
|
|
313
349
|
|
|
314
350
|
It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the bases for various reports. You can pass a string or symbol into a `Report` instead of a `Metric` to look up an pre-made metric. This is done by passing the symbol or string into the `lookup` class method on the constant defined in `ActiveReporting::Configuration.metric_lookup_class`.
|
|
@@ -331,6 +367,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
331
367
|
|
|
332
368
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
333
369
|
|
|
370
|
+
## Testing
|
|
371
|
+
|
|
372
|
+
You can run the test suite using `rake test`. To test against a particular database, you'll need to set the
|
|
373
|
+
appropriate `DB` environment variable, e.g. `DB=pg rake test`.
|
|
374
|
+
|
|
334
375
|
## Contributing
|
|
335
376
|
|
|
336
377
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_reporting. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
@@ -339,4 +380,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
|
|
|
339
380
|
## License
|
|
340
381
|
|
|
341
382
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
342
|
-
|
data/Rakefile
CHANGED
data/active_reporting.gemspec
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require 'active_reporting/version'
|
|
5
6
|
|
|
@@ -21,16 +22,16 @@ Gem::Specification.new do |spec|
|
|
|
21
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
23
|
spec.require_paths = ['lib']
|
|
23
24
|
|
|
24
|
-
spec.required_ruby_version = '>= 2.
|
|
25
|
+
spec.required_ruby_version = '>= 2.3'
|
|
25
26
|
|
|
26
|
-
spec.add_dependency 'activerecord'
|
|
27
|
-
spec.add_dependency 'activesupport'
|
|
27
|
+
spec.add_dependency 'activerecord'
|
|
28
|
+
spec.add_dependency 'activesupport'
|
|
28
29
|
|
|
29
30
|
spec.add_development_dependency 'bundler'
|
|
30
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
|
31
31
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
32
|
+
spec.add_development_dependency 'mysql2'
|
|
33
|
+
spec.add_development_dependency 'pg'
|
|
34
|
+
spec.add_development_dependency 'rake'
|
|
32
35
|
spec.add_development_dependency 'ransack'
|
|
33
36
|
spec.add_development_dependency 'sqlite3'
|
|
34
|
-
spec.add_development_dependency 'pg'
|
|
35
|
-
spec.add_development_dependency 'mysql2'
|
|
36
37
|
end
|
data/bin/console
CHANGED
data/lib/active_reporting.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_record'
|
|
2
4
|
require 'active_reporting/active_record_adaptor'
|
|
3
5
|
require 'active_reporting/configuration'
|
|
@@ -12,7 +14,7 @@ require 'active_reporting/version'
|
|
|
12
14
|
begin
|
|
13
15
|
require 'ransack'
|
|
14
16
|
ActiveReporting::Configuration.ransack_available = true
|
|
15
|
-
rescue
|
|
17
|
+
rescue LoadError, StandardError
|
|
16
18
|
ActiveReporting::Configuration.ransack_available = false
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -23,8 +25,8 @@ module ActiveReporting
|
|
|
23
25
|
klass = Configuration.metric_lookup_class
|
|
24
26
|
unless defined?(klass.constantize)
|
|
25
27
|
raise BadMetricLookupClass,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
"#{klass} not defined. Please define a class responsible for looking up a metric by name." \
|
|
29
|
+
' You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`.'
|
|
28
30
|
end
|
|
29
31
|
unless klass.constantize.respond_to?(:lookup)
|
|
30
32
|
raise BadMetricLookupClass, "#{klass} needs to define a class method called 'lookup'"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveReporting
|
|
2
4
|
# This is included into every class that inherits from ActiveRecord::Base
|
|
3
5
|
module ActiveRecordAdaptor
|
|
@@ -13,7 +15,7 @@ module ActiveReporting
|
|
|
13
15
|
const_name.constantize
|
|
14
16
|
rescue NameError
|
|
15
17
|
const = Object.const_set(const_name, Class.new(ActiveReporting::FactModel))
|
|
16
|
-
const.model= self
|
|
18
|
+
const.model = self
|
|
17
19
|
const
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveReporting
|
|
2
4
|
class Dimension
|
|
5
|
+
TYPES = { degenerate: :degenerate, standard: :standard }.freeze
|
|
3
6
|
attr_reader :name
|
|
4
7
|
|
|
5
8
|
# @param model [ActiveRecord::Base]
|
|
@@ -19,14 +22,21 @@ module ActiveReporting
|
|
|
19
22
|
# @return [Symbol]
|
|
20
23
|
def type
|
|
21
24
|
@type ||= if model.column_names.include?(@name)
|
|
22
|
-
:degenerate
|
|
25
|
+
TYPES[:degenerate]
|
|
23
26
|
elsif association
|
|
24
|
-
:standard
|
|
27
|
+
TYPES[:standard]
|
|
25
28
|
else
|
|
26
29
|
raise UnknownDimension, "Dimension '#{@name}' not found on fact model '#{@fact_model}'"
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
# Whether the dimension is a datetime column
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def datetime?
|
|
37
|
+
@datetime ||= type == TYPES[:degenerate] && model.column_for_attribute(@name).type == :datetime
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Tells if the dimension is hierarchical
|
|
31
41
|
#
|
|
32
42
|
# @return [Boolean]
|
|
@@ -53,7 +63,7 @@ module ActiveReporting
|
|
|
53
63
|
#
|
|
54
64
|
# @return [ActiveRecord::Reflection]
|
|
55
65
|
def association
|
|
56
|
-
@
|
|
66
|
+
@association ||= model.reflect_on_association(@name)
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveReporting
|
|
2
4
|
class FactModel
|
|
3
5
|
class << self
|
|
@@ -109,7 +111,7 @@ module ActiveReporting
|
|
|
109
111
|
# @param body [Lambda]
|
|
110
112
|
def self.dimension_label_callback(column, body)
|
|
111
113
|
@dimension_label_callbacks ||= {}
|
|
112
|
-
raise ArgumentError,
|
|
114
|
+
raise ArgumentError, 'Dimension label callback body must be a callable object' unless body.respond_to?(:call)
|
|
113
115
|
@dimension_label_callbacks[column.to_sym] = body
|
|
114
116
|
end
|
|
115
117
|
|
|
@@ -143,7 +145,9 @@ module ActiveReporting
|
|
|
143
145
|
# Invoke this method to make all dimension filters fallback to use ransack
|
|
144
146
|
# if they are not defined as scopes on the model
|
|
145
147
|
def self.use_ransack_for_unknown_dimension_filters
|
|
146
|
-
|
|
148
|
+
unless Configuration.ransack_available
|
|
149
|
+
raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
|
|
150
|
+
end
|
|
147
151
|
@ransack_fallback = true
|
|
148
152
|
end
|
|
149
153
|
|
|
@@ -163,7 +167,7 @@ module ActiveReporting
|
|
|
163
167
|
@dimension_filters ||= {}
|
|
164
168
|
dm = @dimension_filters[name.to_sym]
|
|
165
169
|
return dm if dm.present?
|
|
166
|
-
return @dimension_filters[name.to_sym] = DimensionFilter.build(
|
|
170
|
+
return @dimension_filters[name.to_sym] = DimensionFilter.build(name, :ransack) if ransack_fallback
|
|
167
171
|
raise UnknownDimensionFilter, "Dimension filter '#{name}' not found on fact model '#{self.name}'"
|
|
168
172
|
end
|
|
169
173
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
module ActiveReporting
|
|
3
5
|
class Report
|
|
@@ -34,7 +36,7 @@ module ActiveReporting
|
|
|
34
36
|
private ######################################################################
|
|
35
37
|
|
|
36
38
|
def build_data
|
|
37
|
-
@data = model.connection.exec_query(statement.to_sql).
|
|
39
|
+
@data = model.connection.exec_query(statement.to_sql).to_a
|
|
38
40
|
apply_dimension_callbacks
|
|
39
41
|
@data
|
|
40
42
|
end
|
|
@@ -1,23 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
module ActiveReporting
|
|
3
5
|
class ReportingDimension
|
|
4
6
|
extend Forwardable
|
|
5
|
-
|
|
7
|
+
SUPPORTED_DBS = %w[PostgreSQL PostGIS].freeze
|
|
8
|
+
# Values for the Postgres `date_trunc` method.
|
|
9
|
+
# See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
|
|
10
|
+
DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
|
|
11
|
+
century millennium].freeze
|
|
12
|
+
def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
|
|
6
13
|
|
|
7
14
|
def self.build_from_dimensions(fact_model, dimensions)
|
|
8
15
|
Array(dimensions).map do |dim|
|
|
9
16
|
dimension_name, label = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
|
|
10
17
|
found_dimension = fact_model.dimensions[dimension_name.to_sym]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
new(found_dimension, label
|
|
18
|
+
|
|
19
|
+
raise(UnknownDimension, "Dimension '#{dim}' not found on fact model '#{fact_model}'") if found_dimension.nil?
|
|
20
|
+
|
|
21
|
+
new(found_dimension, label_config(label))
|
|
15
22
|
end
|
|
16
23
|
end
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
# If you pass a symbol it means you just indicate
|
|
26
|
+
# the field on that dimension. With a hash you can
|
|
27
|
+
# customize the name of the label
|
|
28
|
+
#
|
|
29
|
+
# @param [Symbol|Hash] label
|
|
30
|
+
def self.label_config(label)
|
|
31
|
+
return { label: label } unless label.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
label: label[:field],
|
|
35
|
+
label_name: label[:name]
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param dimension [ActiveReporting::Dimension]
|
|
40
|
+
# @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
|
|
41
|
+
# @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
|
|
42
|
+
def initialize(dimension, label: nil, label_name: nil)
|
|
19
43
|
@dimension = dimension
|
|
20
|
-
|
|
44
|
+
|
|
45
|
+
determine_label_field(label)
|
|
46
|
+
determine_label_name(label_name)
|
|
21
47
|
end
|
|
22
48
|
|
|
23
49
|
# The foreign key to use in queries
|
|
@@ -31,10 +57,10 @@ module ActiveReporting
|
|
|
31
57
|
#
|
|
32
58
|
# @return [Array]
|
|
33
59
|
def select_statement(with_identifier: true)
|
|
34
|
-
return [
|
|
60
|
+
return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
|
|
35
61
|
|
|
36
|
-
ss = ["#{label_fragment} AS #{
|
|
37
|
-
ss << "#{identifier_fragment} AS #{name}_identifier" if with_identifier
|
|
62
|
+
ss = ["#{label_fragment} AS #{model.connection.quote_column_name(@label_name)}"]
|
|
63
|
+
ss << "#{identifier_fragment} AS #{model.connection.quote_column_name("#{name}_identifier")}" if with_identifier
|
|
38
64
|
ss
|
|
39
65
|
end
|
|
40
66
|
|
|
@@ -42,7 +68,7 @@ module ActiveReporting
|
|
|
42
68
|
#
|
|
43
69
|
# @return [Array]
|
|
44
70
|
def group_by_statement(with_identifier: true)
|
|
45
|
-
return [degenerate_fragment] if type == :degenerate
|
|
71
|
+
return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
|
|
46
72
|
|
|
47
73
|
group = [label_fragment]
|
|
48
74
|
group << identifier_fragment if with_identifier
|
|
@@ -54,8 +80,8 @@ module ActiveReporting
|
|
|
54
80
|
# @return [String]
|
|
55
81
|
def order_by_statement(direction:)
|
|
56
82
|
direction = direction.to_s.upcase
|
|
57
|
-
raise "Ording direction should be 'asc' or 'desc'" unless %w
|
|
58
|
-
return "#{degenerate_fragment} #{direction}" if type == :degenerate
|
|
83
|
+
raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
|
|
84
|
+
return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
|
|
59
85
|
"#{label_fragment} #{direction}"
|
|
60
86
|
end
|
|
61
87
|
|
|
@@ -68,31 +94,67 @@ module ActiveReporting
|
|
|
68
94
|
|
|
69
95
|
private ####################################################################
|
|
70
96
|
|
|
71
|
-
def
|
|
72
|
-
@label =
|
|
73
|
-
|
|
97
|
+
def determine_label_field(label_field)
|
|
98
|
+
@label = if label_field.present? && validate_hierarchical_label(label_field)
|
|
99
|
+
label_field.to_sym
|
|
100
|
+
else
|
|
101
|
+
dimension_fact_model.dimension_label || Configuration.default_dimension_label
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def determine_label_name(label_name)
|
|
106
|
+
@label_name = label_name ? "#{name}_#{label_name}" : name
|
|
74
107
|
end
|
|
75
108
|
|
|
76
109
|
def validate_hierarchical_label(hierarchical_label)
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
if datetime?
|
|
111
|
+
validate_supported_database_for_datetime_hierarchies
|
|
112
|
+
validate_against_datetime_hierarchies(hierarchical_label)
|
|
113
|
+
else
|
|
114
|
+
validate_dimension_is_hierachical(hierarchical_label)
|
|
115
|
+
validate_against_fact_model_properties(hierarchical_label)
|
|
82
116
|
end
|
|
83
117
|
true
|
|
84
118
|
end
|
|
85
119
|
|
|
120
|
+
def validate_dimension_is_hierachical(hierarchical_label)
|
|
121
|
+
return if hierarchical?
|
|
122
|
+
raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_supported_database_for_datetime_hierarchies
|
|
126
|
+
return if SUPPORTED_DBS.include?(model.connection.adapter_name)
|
|
127
|
+
raise InvalidDimensionLabel,
|
|
128
|
+
"Cannot utilize datetime grouping for #{name}; " \
|
|
129
|
+
"database #{model.connection.adapter_name} is not supported"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_against_datetime_hierarchies(hierarchical_label)
|
|
133
|
+
return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
|
|
134
|
+
raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate_against_fact_model_properties(hierarchical_label)
|
|
138
|
+
return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
|
|
139
|
+
raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
|
|
140
|
+
end
|
|
141
|
+
|
|
86
142
|
def degenerate_fragment
|
|
143
|
+
return "#{name}_#{@label}" if datetime?
|
|
144
|
+
"#{model.quoted_table_name}.#{name}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def degenerate_select_fragment
|
|
148
|
+
return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
|
|
87
149
|
"#{model.quoted_table_name}.#{name}"
|
|
88
150
|
end
|
|
89
151
|
|
|
90
152
|
def identifier_fragment
|
|
91
|
-
"#{klass.quoted_table_name}.#{klass.primary_key}"
|
|
153
|
+
"#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
|
|
92
154
|
end
|
|
93
155
|
|
|
94
156
|
def label_fragment
|
|
95
|
-
"#{klass.quoted_table_name}.#{@label}"
|
|
157
|
+
"#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
|
|
96
158
|
end
|
|
97
159
|
|
|
98
160
|
def dimension_fact_model
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_reporting
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tony Drake
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: '0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: '0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: activesupport
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: '0'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: '0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: bundler
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,35 +53,35 @@ dependencies:
|
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
56
|
+
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
61
|
+
version: '5.0'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '5.0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
70
|
+
name: mysql2
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
75
|
+
version: '0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
84
|
+
name: pg
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - ">="
|
|
@@ -95,7 +95,7 @@ dependencies:
|
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
98
|
+
name: rake
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - ">="
|
|
@@ -109,7 +109,7 @@ dependencies:
|
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '0'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
112
|
+
name: ransack
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - ">="
|
|
@@ -123,7 +123,7 @@ dependencies:
|
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '0'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
126
|
+
name: sqlite3
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
128
128
|
requirements:
|
|
129
129
|
- - ">="
|
|
@@ -136,7 +136,7 @@ dependencies:
|
|
|
136
136
|
- - ">="
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '0'
|
|
139
|
-
description:
|
|
139
|
+
description:
|
|
140
140
|
email:
|
|
141
141
|
- t27duck@gmail.com
|
|
142
142
|
executables: []
|
|
@@ -170,7 +170,7 @@ homepage: https://github.com/t27duck/active_reporting
|
|
|
170
170
|
licenses:
|
|
171
171
|
- MIT
|
|
172
172
|
metadata: {}
|
|
173
|
-
post_install_message:
|
|
173
|
+
post_install_message:
|
|
174
174
|
rdoc_options: []
|
|
175
175
|
require_paths:
|
|
176
176
|
- lib
|
|
@@ -178,16 +178,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
178
178
|
requirements:
|
|
179
179
|
- - ">="
|
|
180
180
|
- !ruby/object:Gem::Version
|
|
181
|
-
version: '2.
|
|
181
|
+
version: '2.3'
|
|
182
182
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
183
183
|
requirements:
|
|
184
184
|
- - ">="
|
|
185
185
|
- !ruby/object:Gem::Version
|
|
186
186
|
version: '0'
|
|
187
187
|
requirements: []
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
signing_key:
|
|
188
|
+
rubygems_version: 3.0.3
|
|
189
|
+
signing_key:
|
|
191
190
|
specification_version: 4
|
|
192
191
|
summary: Add relational OLAP-like functionality for ActiveRecord
|
|
193
192
|
test_files: []
|