reports_kit 0.0.1
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 +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +83 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +468 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kit/application.js +14 -0
- data/app/assets/javascripts/reports_kit/lib/_init.js +8 -0
- data/app/assets/javascripts/reports_kit/lib/chart.js +39 -0
- data/app/assets/javascripts/reports_kit/lib/report.js +119 -0
- data/app/assets/javascripts/reports_kit/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kit/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kit/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kit/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kit/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kit/reports.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2.css +484 -0
- data/config/routes.rb +10 -0
- data/docs/images/chart_options.png +0 -0
- data/docs/images/dashed_line.png +0 -0
- data/docs/images/flights_by_carrier.png +0 -0
- data/docs/images/flights_by_carrier_and_flight_at.png +0 -0
- data/docs/images/flights_by_delay.png +0 -0
- data/docs/images/flights_by_flight_at.png +0 -0
- data/docs/images/flights_by_hours_delayed.png +0 -0
- data/docs/images/flights_with_check_box.png +0 -0
- data/docs/images/flights_with_configured_boolean.png +0 -0
- data/docs/images/flights_with_configured_datetime.png +0 -0
- data/docs/images/flights_with_configured_number.png +0 -0
- data/docs/images/flights_with_configured_string.png +0 -0
- data/docs/images/flights_with_date_range.png +0 -0
- data/docs/images/flights_with_filters.png +0 -0
- data/docs/images/flights_with_multi_autocomplete.png +0 -0
- data/docs/images/flights_with_string_filter.png +0 -0
- data/docs/images/horizontal_bar.png +0 -0
- data/docs/images/legend_right.png +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gists/doc.txt +58 -0
- data/lib/reports_kit.rb +17 -0
- data/lib/reports_kit/base_controller.rb +11 -0
- data/lib/reports_kit/configuration.rb +10 -0
- data/lib/reports_kit/engine.rb +21 -0
- data/lib/reports_kit/helper.rb +19 -0
- data/lib/reports_kit/model.rb +16 -0
- data/lib/reports_kit/model_configuration.rb +23 -0
- data/lib/reports_kit/rails.rb +5 -0
- data/lib/reports_kit/report_builder.rb +76 -0
- data/lib/reports_kit/reports/data/chart_options.rb +132 -0
- data/lib/reports_kit/reports/data/generate.rb +65 -0
- data/lib/reports_kit/reports/data/one_dimension.rb +71 -0
- data/lib/reports_kit/reports/data/two_dimensions.rb +129 -0
- data/lib/reports_kit/reports/data/utils.rb +79 -0
- data/lib/reports_kit/reports/dimension.rb +78 -0
- data/lib/reports_kit/reports/filter.rb +84 -0
- data/lib/reports_kit/reports/filter_types/base.rb +47 -0
- data/lib/reports_kit/reports/filter_types/boolean.rb +30 -0
- data/lib/reports_kit/reports/filter_types/datetime.rb +27 -0
- data/lib/reports_kit/reports/filter_types/number.rb +28 -0
- data/lib/reports_kit/reports/filter_types/records.rb +26 -0
- data/lib/reports_kit/reports/filter_types/string.rb +38 -0
- data/lib/reports_kit/reports/generate_autocomplete_results.rb +55 -0
- data/lib/reports_kit/reports/inferrable_configuration.rb +113 -0
- data/lib/reports_kit/reports/measure.rb +58 -0
- data/lib/reports_kit/reports_controller.rb +15 -0
- data/lib/reports_kit/resources_controller.rb +8 -0
- data/lib/reports_kit/version.rb +3 -0
- data/reports_kit.gemspec +23 -0
- data/spec/factories/issue_factory.rb +4 -0
- data/spec/factories/issues_label_factory.rb +4 -0
- data/spec/factories/label_factory.rb +4 -0
- data/spec/factories/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +35 -0
- data/spec/fixtures/generate_outputs.yml +208 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +275 -0
- data/spec/reports_kit/reports/dimension_spec.rb +38 -0
- data/spec/reports_kit/reports/filter_spec.rb +38 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +13 -0
- data/spec/support/models/issue.rb +6 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/repo.rb +7 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +38 -0
- metadata +232 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class Generate
|
|
5
|
+
ROUND_PRECISION = 3
|
|
6
|
+
|
|
7
|
+
attr_accessor :properties, :context_record
|
|
8
|
+
|
|
9
|
+
def initialize(properties, context_record: nil)
|
|
10
|
+
self.properties = properties.deep_symbolize_keys
|
|
11
|
+
self.context_record = context_record
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform
|
|
15
|
+
if second_dimension
|
|
16
|
+
data = Data::TwoDimensions.new(measure, dimension, second_dimension).perform
|
|
17
|
+
else
|
|
18
|
+
data = Data::OneDimension.new(measure, dimension).perform
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ChartOptions.new(data, options: properties[:chart], inferred_options: inferred_options).perform
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def measure
|
|
27
|
+
@measure ||= begin
|
|
28
|
+
measure_hash = properties[:measure]
|
|
29
|
+
raise ArgumentError.new('The number of measures must be exactly one') if measure_hash.blank?
|
|
30
|
+
Measure.new(measure_hash, context_record: context_record)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dimension
|
|
35
|
+
@dimension ||= begin
|
|
36
|
+
Dimension.new(dimension_hashes[0], measure: measure)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def second_dimension
|
|
41
|
+
@second_dimension ||= begin
|
|
42
|
+
Dimension.new(dimension_hashes[1], measure: measure) if dimension_hashes[1]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dimension_hashes
|
|
47
|
+
@dimension_hashes ||= begin
|
|
48
|
+
dimension_hashes = properties[:dimensions]
|
|
49
|
+
raise ArgumentError.new('Blank dimensions') if dimension_hashes.blank?
|
|
50
|
+
raise ArgumentError.new('The number of dimensions must be 1-2') unless dimension_hashes.length.in?([1, 2])
|
|
51
|
+
dimension_hashes = dimension_hashes.values if dimension_hashes.is_a?(Hash) && dimension_hashes.key?(:'0')
|
|
52
|
+
dimension_hashes
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inferred_options
|
|
57
|
+
{
|
|
58
|
+
x_axis_label: dimension.label,
|
|
59
|
+
y_axis_label: measure.label
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class OneDimension
|
|
5
|
+
attr_accessor :measure, :dimension
|
|
6
|
+
|
|
7
|
+
def initialize(measure, dimension)
|
|
8
|
+
self.measure = measure
|
|
9
|
+
self.dimension = dimension
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def perform
|
|
13
|
+
{
|
|
14
|
+
chart_data: {
|
|
15
|
+
labels: labels,
|
|
16
|
+
datasets: datasets
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def dimension_keys_values
|
|
24
|
+
@dimension_keys_values ||= begin
|
|
25
|
+
relation = measure.filtered_relation
|
|
26
|
+
relation = relation.group(dimension.group_expression)
|
|
27
|
+
relation = relation.joins(dimension.joins) if dimension.joins
|
|
28
|
+
relation = relation.limit(dimension.dimension_instances_limit) if dimension.dimension_instances_limit
|
|
29
|
+
if dimension.should_be_sorted_by_count?
|
|
30
|
+
relation = relation.order('1 DESC')
|
|
31
|
+
else
|
|
32
|
+
relation = relation.order('2')
|
|
33
|
+
end
|
|
34
|
+
dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
|
|
35
|
+
dimension_keys_values = Utils.populate_sparse_values(dimension_keys_values)
|
|
36
|
+
dimension_keys_values.delete(nil)
|
|
37
|
+
dimension_keys_values.delete('')
|
|
38
|
+
dimension_keys_values
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def datasets
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
label: measure.label,
|
|
46
|
+
data: values
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def values
|
|
52
|
+
dimension_keys_values.values.map { |value| value.round(Generate::ROUND_PRECISION) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def labels
|
|
56
|
+
keys = dimension_keys_values.keys
|
|
57
|
+
keys.map do |key|
|
|
58
|
+
Utils.dimension_key_to_label(key, dimension, dimension_ids_dimension_instances)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dimension_ids_dimension_instances
|
|
63
|
+
@dimension_ids_dimension_instances ||= begin
|
|
64
|
+
dimension_ids = dimension_keys_values.keys
|
|
65
|
+
Utils.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class TwoDimensions
|
|
5
|
+
attr_accessor :measure, :dimension, :second_dimension
|
|
6
|
+
|
|
7
|
+
def initialize(measure, dimension, second_dimension)
|
|
8
|
+
self.measure = measure
|
|
9
|
+
self.dimension = dimension
|
|
10
|
+
self.second_dimension = second_dimension
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def perform
|
|
14
|
+
{
|
|
15
|
+
chart_data: {
|
|
16
|
+
labels: labels,
|
|
17
|
+
datasets: datasets
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def dimension_keys_values
|
|
25
|
+
@dimension_keys_values ||= begin
|
|
26
|
+
relation = measure.filtered_relation
|
|
27
|
+
relation = measure.conditions.call(relation) if measure.conditions
|
|
28
|
+
relation = relation.group(dimension.group_expression, second_dimension.group_expression)
|
|
29
|
+
|
|
30
|
+
relation = relation.joins(dimension.joins) if dimension.joins
|
|
31
|
+
relation = relation.joins(second_dimension.joins) if second_dimension.joins
|
|
32
|
+
|
|
33
|
+
if dimension.should_be_sorted_by_count?
|
|
34
|
+
relation = relation.order('1 DESC')
|
|
35
|
+
else
|
|
36
|
+
relation = relation.order('2')
|
|
37
|
+
end
|
|
38
|
+
dimension_keys_values = relation.count
|
|
39
|
+
|
|
40
|
+
if dimension.should_be_sorted_by_count?
|
|
41
|
+
dimension_keys_values = sort_dimension_keys_values_by_count(dimension_keys_values)
|
|
42
|
+
end
|
|
43
|
+
Hash[dimension_keys_values]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def primary_keys_secondary_keys_values
|
|
48
|
+
@primary_keys_secondary_keys_values ||= begin
|
|
49
|
+
primary_keys_secondary_keys_values = {}
|
|
50
|
+
dimension_keys_values.each do |(primary_key, secondary_key), value|
|
|
51
|
+
primary_keys_secondary_keys_values[primary_key] ||= {}
|
|
52
|
+
primary_keys_secondary_keys_values[primary_key][secondary_key] = value
|
|
53
|
+
end
|
|
54
|
+
primary_keys_secondary_keys_values
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sort_dimension_keys_values_by_count(dimension_keys_values)
|
|
59
|
+
primary_keys_counts = Hash.new(0)
|
|
60
|
+
dimension_keys_values.each do |(primary_key, secondary_key), count|
|
|
61
|
+
primary_keys_counts[primary_key] += count
|
|
62
|
+
end
|
|
63
|
+
primary_keys_counts = primary_keys_counts.to_a
|
|
64
|
+
sorted_primary_keys = primary_keys_counts.sort_by { |primary_key, count| count }.reverse.map(&:first)
|
|
65
|
+
dimension_keys_values = dimension_keys_values.sort_by { |(primary_key, secondary_key), count| sorted_primary_keys.index(primary_key) }
|
|
66
|
+
Hash[dimension_keys_values]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dimension_ids
|
|
70
|
+
dimension_keys_values.keys.map(&:first)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dimension_ids_dimension_instances
|
|
74
|
+
@dimension_ids_dimension_instances ||= begin
|
|
75
|
+
Utils.dimension_to_dimension_ids_dimension_instances(dimension, dimension_keys_values.keys.map(&:first))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def second_dimension_ids_dimension_instances
|
|
80
|
+
@second_dimension_ids_dimension_instances ||= begin
|
|
81
|
+
Utils.dimension_to_dimension_ids_dimension_instances(second_dimension, dimension_keys_values.keys.map(&:last))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def datasets
|
|
86
|
+
secondary_keys_values = secondary_keys.map do |secondary_key|
|
|
87
|
+
values = primary_keys.map do |primary_key|
|
|
88
|
+
primary_keys_secondary_keys_values[primary_key].try(:[], secondary_key) || 0
|
|
89
|
+
end
|
|
90
|
+
[secondary_key, values]
|
|
91
|
+
end
|
|
92
|
+
secondary_keys_values = secondary_keys_values.sort_by { |_, values| values.sum }.reverse
|
|
93
|
+
secondary_keys_values.map do |secondary_key, values|
|
|
94
|
+
{
|
|
95
|
+
label: Utils.dimension_key_to_label(secondary_key, second_dimension, second_dimension_ids_dimension_instances),
|
|
96
|
+
data: values
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def primary_keys
|
|
102
|
+
@primary_keys ||= begin
|
|
103
|
+
keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:first).uniq)
|
|
104
|
+
if dimension.should_be_sorted_by_count?
|
|
105
|
+
limit = dimension.dimension_instances_limit
|
|
106
|
+
keys = keys.first(limit) if limit
|
|
107
|
+
end
|
|
108
|
+
keys
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def secondary_keys
|
|
113
|
+
@secondary_keys ||= begin
|
|
114
|
+
keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:last).uniq)
|
|
115
|
+
limit = second_dimension.dimension_instances_limit
|
|
116
|
+
keys = keys.first(limit) if limit
|
|
117
|
+
keys
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def labels
|
|
122
|
+
primary_keys.map do |primary_key|
|
|
123
|
+
Utils.dimension_key_to_label(primary_key, dimension, dimension_ids_dimension_instances)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class Utils
|
|
5
|
+
def self.format_time(time)
|
|
6
|
+
time.strftime('%b %-d, \'%y')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.populate_sparse_values(dimension_keys_values, use_first_value_key: false)
|
|
10
|
+
return dimension_keys_values if dimension_keys_values.blank?
|
|
11
|
+
first_key = dimension_keys_values.first.first
|
|
12
|
+
return dimension_keys_values unless first_key.is_a?(Time)
|
|
13
|
+
|
|
14
|
+
beginning_of_current_week = Time.now.utc.beginning_of_week(ReportsKit.configuration.first_day_of_week)
|
|
15
|
+
last_key = dimension_keys_values.to_a.last.first
|
|
16
|
+
last_key = [beginning_of_current_week, last_key].compact.max
|
|
17
|
+
|
|
18
|
+
time = first_key
|
|
19
|
+
full_dimension_instances_values = []
|
|
20
|
+
if use_first_value_key
|
|
21
|
+
first_value_key = dimension_keys_values.first.last.keys.first
|
|
22
|
+
blank_value = { first_value_key => 0 }
|
|
23
|
+
else
|
|
24
|
+
blank_value = 0
|
|
25
|
+
end
|
|
26
|
+
loop do
|
|
27
|
+
full_dimension_instances_values << [time, dimension_keys_values[time] || blank_value]
|
|
28
|
+
break if time >= last_key
|
|
29
|
+
time += 1.week
|
|
30
|
+
end
|
|
31
|
+
Hash[full_dimension_instances_values]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.populate_sparse_keys(keys)
|
|
35
|
+
return keys if keys.blank?
|
|
36
|
+
first_key = keys.first
|
|
37
|
+
return keys unless first_key.is_a?(Time)
|
|
38
|
+
keys = keys.sort
|
|
39
|
+
beginning_of_current_week = Time.now.utc.beginning_of_week(ReportsKit.configuration.first_day_of_week)
|
|
40
|
+
last_key = keys.last
|
|
41
|
+
last_key = [beginning_of_current_week, last_key].compact.max
|
|
42
|
+
|
|
43
|
+
time = first_key
|
|
44
|
+
populated_keys = []
|
|
45
|
+
loop do
|
|
46
|
+
populated_keys << time
|
|
47
|
+
break if time >= last_key
|
|
48
|
+
time += 1.week
|
|
49
|
+
end
|
|
50
|
+
populated_keys
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
|
|
54
|
+
return nil unless dimension.instance_class
|
|
55
|
+
dimension_instances = dimension.instance_class.where(id: dimension_ids.uniq)
|
|
56
|
+
dimension_ids_dimension_instances = dimension_instances.map do |dimension_instance|
|
|
57
|
+
[dimension_instance.id, dimension_instance]
|
|
58
|
+
end
|
|
59
|
+
Hash[dimension_ids_dimension_instances]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.dimension_key_to_label(dimension_instance, dimension, ids_dimension_instances)
|
|
63
|
+
return dimension_instance.to_s if dimension.configured_by_column? && dimension.column_type == :integer
|
|
64
|
+
case dimension_instance
|
|
65
|
+
when Time
|
|
66
|
+
Utils.format_time(dimension_instance)
|
|
67
|
+
when Fixnum
|
|
68
|
+
raise ArgumentError.new("ids_dimension_instances must be present for Dimension with identifier: #{dimension_instance}") unless ids_dimension_instances
|
|
69
|
+
instance = ids_dimension_instances[dimension_instance.to_i]
|
|
70
|
+
raise ArgumentError.new("instance could not be found for Dimension with identifier: #{dimension_instance}") unless instance
|
|
71
|
+
instance.to_s
|
|
72
|
+
else
|
|
73
|
+
dimension_instance.to_s.gsub(/\.0$/, '')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
class Dimension
|
|
4
|
+
DEFAULT_DIMENSION_INSTANCES_LIMIT = 30
|
|
5
|
+
|
|
6
|
+
attr_accessor :properties, :measure, :configuration
|
|
7
|
+
|
|
8
|
+
delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
|
|
9
|
+
:settings_from_model, :reflection, :instance_class, :model_class, :column_type,
|
|
10
|
+
to: :configuration
|
|
11
|
+
|
|
12
|
+
def initialize(properties, measure:)
|
|
13
|
+
self.configuration = InferrableConfiguration.new(self, :dimensions)
|
|
14
|
+
self.measure = measure
|
|
15
|
+
|
|
16
|
+
raise ArgumentError.new('Blank properties') if properties.blank?
|
|
17
|
+
properties = { key: properties } if properties.is_a?(String)
|
|
18
|
+
raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
|
|
19
|
+
properties = properties.deep_symbolize_keys
|
|
20
|
+
self.properties = properties
|
|
21
|
+
missing_group_setting = settings && !settings.key?(:group)
|
|
22
|
+
raise ArgumentError.new("Dimension settings for dimension '#{key}' of #{model_class} must include :group") if missing_group_setting
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def key
|
|
26
|
+
properties[:key]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def label
|
|
30
|
+
key.titleize
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def settings
|
|
34
|
+
inferred_settings.merge(settings_from_model)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inferred_settings
|
|
38
|
+
configuration.inferred_settings.merge(inferred_dimension_settings)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def inferred_dimension_settings
|
|
42
|
+
{
|
|
43
|
+
group: group_expression
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def group_expression
|
|
48
|
+
if configured_by_model?
|
|
49
|
+
settings_from_model[:group]
|
|
50
|
+
elsif configured_by_association?
|
|
51
|
+
"#{model_class.table_name}.#{reflection.foreign_key}"
|
|
52
|
+
elsif configured_by_column? && configured_by_time?
|
|
53
|
+
"date_trunc('week', #{model_class.table_name}.#{key}::timestamp)"
|
|
54
|
+
elsif configured_by_column?
|
|
55
|
+
"#{model_class.table_name}.#{key}"
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError.new('Invalid group_expression')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def joins
|
|
62
|
+
settings_from_model[:joins] if configured_by_model?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dimension_instances_limit
|
|
66
|
+
if configured_by_time?
|
|
67
|
+
properties[:limit]
|
|
68
|
+
else
|
|
69
|
+
properties[:limit] || DEFAULT_DIMENSION_INSTANCES_LIMIT
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def should_be_sorted_by_count?
|
|
74
|
+
!configured_by_time?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
class Filter
|
|
4
|
+
CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES = {
|
|
5
|
+
association: FilterTypes::Records,
|
|
6
|
+
boolean: FilterTypes::Boolean,
|
|
7
|
+
datetime: FilterTypes::Datetime,
|
|
8
|
+
integer: FilterTypes::Number,
|
|
9
|
+
string: FilterTypes::String
|
|
10
|
+
}
|
|
11
|
+
COLUMN_TYPES_FILTER_TYPE_CLASSES = {
|
|
12
|
+
boolean: FilterTypes::Boolean,
|
|
13
|
+
datetime: FilterTypes::Datetime,
|
|
14
|
+
integer: FilterTypes::Number,
|
|
15
|
+
string: FilterTypes::String
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
attr_accessor :properties, :measure, :configuration
|
|
19
|
+
|
|
20
|
+
delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
|
|
21
|
+
:settings_from_model, :configuration_strategy, :instance_class, :column_type, :column,
|
|
22
|
+
to: :configuration
|
|
23
|
+
|
|
24
|
+
def initialize(properties, measure:)
|
|
25
|
+
self.configuration = InferrableConfiguration.new(self, :filters)
|
|
26
|
+
self.measure = measure
|
|
27
|
+
|
|
28
|
+
properties = { key: properties } if properties.is_a?(String)
|
|
29
|
+
raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
|
|
30
|
+
self.properties = properties.deep_symbolize_keys
|
|
31
|
+
self.properties[:criteria] = filter_type.default_criteria unless self.properties[:criteria]
|
|
32
|
+
self.properties = self.properties
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key
|
|
36
|
+
properties[:key]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def label
|
|
40
|
+
key.titleize
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def settings
|
|
44
|
+
inferred_settings.merge(settings_from_model)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inferred_settings
|
|
48
|
+
configuration.inferred_settings.merge(inferred_filter_settings)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def inferred_filter_settings
|
|
52
|
+
{
|
|
53
|
+
column: column
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def type_klass
|
|
58
|
+
type_klass_for_configuration_strategy = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[configuration_strategy]
|
|
59
|
+
return type_klass_for_configuration_strategy if type_klass_for_configuration_strategy
|
|
60
|
+
type_klass_for_column_type = COLUMN_TYPES_FILTER_TYPE_CLASSES[column_type]
|
|
61
|
+
return type_klass_for_column_type if type_klass_for_column_type
|
|
62
|
+
return filter_type_class_from_model if configured_by_model?
|
|
63
|
+
raise ArgumentError.new("No configuration found for filter with key: '#{key}'")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def filter_type
|
|
67
|
+
type_klass.new(settings, properties)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def filter_type_class_from_model
|
|
71
|
+
return unless settings
|
|
72
|
+
type_key = settings[:type_key]
|
|
73
|
+
raise ArgumentError.new("No type specified for filter with key: '#{key}'") unless type_key
|
|
74
|
+
type_class = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[type_key]
|
|
75
|
+
raise ArgumentError.new("Invalid type ('#{type_key}') specified for filter with key: '#{key}'") unless type_class
|
|
76
|
+
type_class
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply(relation)
|
|
80
|
+
filter_type.apply_filter(relation)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|