active_reporting 0.2.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|