reports_kits 0.7.5 → 0.7.7
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 +4 -4
- data/.gitignore +6 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +21 -0
- data/Appraisals +27 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +35 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kits/application.js +14 -0
- data/app/assets/javascripts/reports_kits/lib/_init.js +9 -0
- data/app/assets/javascripts/reports_kits/lib/chart.js +73 -0
- data/app/assets/javascripts/reports_kits/lib/report.js +135 -0
- data/app/assets/javascripts/reports_kits/lib/table.js +87 -0
- data/app/assets/javascripts/reports_kits/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kits/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kits/vendor/jquery.tablesorter.min.js +4 -0
- data/app/assets/javascripts/reports_kits/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kits/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kits/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kits/reports.css.sass +33 -0
- data/app/assets/stylesheets/reports_kits/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kits/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2.css +484 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +10 -0
- data/docs/images/demo.gif +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gemfiles/mysql.gemfile +7 -0
- data/gemfiles/mysql.gemfile.lock +167 -0
- data/gemfiles/postgresql.gemfile +7 -0
- data/gemfiles/postgresql.gemfile.lock +165 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile +8 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile.lock +168 -0
- data/gemfiles/rails_4_mysql.gemfile +8 -0
- data/gemfiles/rails_4_mysql.gemfile.lock +165 -0
- data/gemfiles/rails_4_postgresql.gemfile +8 -0
- data/gemfiles/rails_4_postgresql.gemfile.lock +163 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile +8 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile.lock +169 -0
- data/gemfiles/rails_5_mysql.gemfile +8 -0
- data/gemfiles/rails_5_mysql.gemfile.lock +171 -0
- data/gemfiles/rails_5_postgresql.gemfile +8 -0
- data/gemfiles/rails_5_postgresql.gemfile.lock +169 -0
- data/lib/reports_kits/base_controller.rb +17 -0
- data/lib/reports_kits/cache.rb +37 -0
- data/lib/reports_kits/configuration.rb +50 -0
- data/lib/reports_kits/engine.rb +21 -0
- data/lib/reports_kits/entity.rb +3 -0
- data/lib/reports_kits/filters_controller.rb +11 -0
- data/lib/reports_kits/form_builder.rb +66 -0
- data/lib/reports_kits/helper.rb +24 -0
- data/lib/reports_kits/model.rb +16 -0
- data/lib/reports_kits/model_configuration.rb +28 -0
- data/lib/reports_kits/normalized_params.rb +16 -0
- data/lib/reports_kits/order.rb +33 -0
- data/lib/reports_kits/relative_time.rb +42 -0
- data/lib/reports_kits/report_builder.rb +88 -0
- data/lib/reports_kits/reports/abstract_series.rb +9 -0
- data/lib/reports_kits/reports/adapters/mysql.rb +26 -0
- data/lib/reports_kits/reports/adapters/postgresql.rb +26 -0
- data/lib/reports_kits/reports/composite_series.rb +48 -0
- data/lib/reports_kits/reports/contextual_filter.rb +19 -0
- data/lib/reports_kits/reports/data/add_table_aggregations.rb +105 -0
- data/lib/reports_kits/reports/data/aggregate_composite.rb +97 -0
- data/lib/reports_kits/reports/data/aggregate_one_dimension.rb +39 -0
- data/lib/reports_kits/reports/data/aggregate_two_dimensions.rb +39 -0
- data/lib/reports_kits/reports/data/chart_data_for_data_method.rb +62 -0
- data/lib/reports_kits/reports/data/chart_options.rb +155 -0
- data/lib/reports_kits/reports/data/format_one_dimension.rb +140 -0
- data/lib/reports_kits/reports/data/format_table.rb +63 -0
- data/lib/reports_kits/reports/data/format_two_dimensions.rb +143 -0
- data/lib/reports_kits/reports/data/generate.rb +156 -0
- data/lib/reports_kits/reports/data/generate_for_properties.rb +97 -0
- data/lib/reports_kits/reports/data/normalize_properties.rb +62 -0
- data/lib/reports_kits/reports/data/populate_one_dimension.rb +54 -0
- data/lib/reports_kits/reports/data/populate_two_dimensions.rb +104 -0
- data/lib/reports_kits/reports/data/utils.rb +178 -0
- data/lib/reports_kits/reports/dimension.rb +27 -0
- data/lib/reports_kits/reports/dimension_with_series.rb +144 -0
- data/lib/reports_kits/reports/filter.rb +29 -0
- data/lib/reports_kits/reports/filter_types/base.rb +48 -0
- data/lib/reports_kits/reports/filter_types/boolean.rb +47 -0
- data/lib/reports_kits/reports/filter_types/datetime.rb +51 -0
- data/lib/reports_kits/reports/filter_types/number.rb +30 -0
- data/lib/reports_kits/reports/filter_types/records.rb +26 -0
- data/lib/reports_kits/reports/filter_types/string.rb +38 -0
- data/lib/reports_kits/reports/filter_with_series.rb +97 -0
- data/lib/reports_kits/reports/generate_autocomplete_method_results.rb +29 -0
- data/lib/reports_kits/reports/generate_autocomplete_results.rb +57 -0
- data/lib/reports_kits/reports/inferrable_configuration.rb +116 -0
- data/lib/reports_kits/reports/model_settings.rb +30 -0
- data/lib/reports_kits/reports/properties.rb +10 -0
- data/lib/reports_kits/reports/properties_to_filter.rb +40 -0
- data/lib/reports_kits/reports/series.rb +121 -0
- data/lib/reports_kits/reports_controller.rb +65 -0
- data/lib/reports_kits/utils.rb +11 -0
- data/lib/reports_kits/value.rb +3 -0
- data/lib/reports_kits/version.rb +3 -0
- data/lib/reports_kits.rb +79 -0
- data/reports_kits.gemspec +26 -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/pro_repo_factory.rb +5 -0
- data/spec/factories/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +254 -0
- data/spec/fixtures/generate_outputs.yml +1216 -0
- data/spec/reports_kit/form_builder_spec.rb +26 -0
- data/spec/reports_kit/relative_time_spec.rb +29 -0
- data/spec/reports_kit/reports/data/generate/contextual_filters_spec.rb +60 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +1371 -0
- data/spec/reports_kit/reports/data/normalize_properties_spec.rb +196 -0
- data/spec/reports_kit/reports/dimension_with_series_spec.rb +67 -0
- data/spec/reports_kit/reports/filter_with_series_spec.rb +39 -0
- data/spec/reports_kit/reports/generate_autocomplete_results_spec.rb +69 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/support/config.rb +41 -0
- data/spec/support/example_data_methods.rb +25 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +25 -0
- data/spec/support/models/issue.rb +14 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/pro/repo.rb +5 -0
- data/spec/support/models/pro/special_issue.rb +4 -0
- data/spec/support/models/repo.rb +13 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +39 -0
- metadata +134 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module ReportsKits
|
|
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, context_params: nil)
|
|
10
|
+
self.properties = properties.deep_symbolize_keys
|
|
11
|
+
self.properties = ReportsKits.configuration.default_properties.deep_merge(self.properties) if ReportsKits.configuration.default_properties
|
|
12
|
+
self.properties[:context_params] = context_params if context_params
|
|
13
|
+
self.properties = NormalizeProperties.new(self.properties).perform
|
|
14
|
+
self.context_record = context_record
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def perform
|
|
18
|
+
data = ReportsKits::Cache.get(properties, context_record)
|
|
19
|
+
return data.deep_symbolize_keys if data
|
|
20
|
+
|
|
21
|
+
data_method_data = ChartDataForDataMethod.new(properties).perform if has_data_method?
|
|
22
|
+
chart_data = data_method_data ? data_method_data[:formatted_data] : properties_to_chart_data
|
|
23
|
+
data = { chart_data: chart_data }
|
|
24
|
+
data = ChartOptions.new(data, options: properties[:chart], inferred_options: inferred_options).perform
|
|
25
|
+
data[:report_options] = report_options if report_options
|
|
26
|
+
data = format_table_data(data, data_method_data) if table_or_csv?
|
|
27
|
+
ReportsKits::Cache.set(properties, context_record, data)
|
|
28
|
+
data
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def has_data_method?
|
|
34
|
+
properties[:data_method].present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def properties_to_chart_data
|
|
38
|
+
if two_dimensions?
|
|
39
|
+
raw_data = Data::FormatTwoDimensions.new(serieses.first, serieses_results.first.last, order: order, limit: limit).perform
|
|
40
|
+
else
|
|
41
|
+
raw_data = Data::FormatOneDimension.new(serieses_results, order: order, limit: limit).perform
|
|
42
|
+
end
|
|
43
|
+
raw_data = format_csv_times(raw_data) if format == 'csv'
|
|
44
|
+
raw_data = Data::AddTableAggregations.new(raw_data, report_options: report_options).perform if table_or_csv?
|
|
45
|
+
raw_data = data_format_method.call(data: raw_data, properties: properties, context_record: context_record) if data_format_method
|
|
46
|
+
raw_data = csv_data_format_method.call(data: raw_data, properties: properties, context_record: context_record) if csv_data_format_method && format == 'csv'
|
|
47
|
+
format_chart_data(raw_data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def format_chart_data(raw_data)
|
|
51
|
+
chart_data = {}
|
|
52
|
+
chart_data[:labels] = raw_data[:entities].map(&:label)
|
|
53
|
+
chart_data[:datasets] = raw_data[:datasets].map do |raw_dataset|
|
|
54
|
+
{
|
|
55
|
+
label: raw_dataset[:entity].label,
|
|
56
|
+
data: raw_dataset[:values].map(&:formatted)
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
chart_data
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def format_table_data(data, data_method_data)
|
|
63
|
+
data[:type] = format
|
|
64
|
+
if has_data_method?
|
|
65
|
+
data[:table_data] = data_method_data[:raw_data]
|
|
66
|
+
return data
|
|
67
|
+
end
|
|
68
|
+
data[:table_data] = Data::FormatTable.new(
|
|
69
|
+
data.delete(:chart_data),
|
|
70
|
+
format: format,
|
|
71
|
+
first_column_label: primary_dimension.try(:label),
|
|
72
|
+
report_options: report_options
|
|
73
|
+
).perform
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def serieses_results
|
|
78
|
+
@serieses_results ||= GenerateForProperties.new(properties, context_record: context_record).perform
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def two_dimensions?
|
|
82
|
+
dimension_keys = serieses_results.first.last.keys
|
|
83
|
+
dimension_keys.first.is_a?(Array)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def order
|
|
87
|
+
@order ||= begin
|
|
88
|
+
return Order.parse(properties[:order]) if properties[:order].present?
|
|
89
|
+
inferred_order
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def limit
|
|
94
|
+
properties[:limit]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def inferred_order
|
|
98
|
+
return Order.new('dimension1', nil, 'asc') if primary_dimension.configured_by_time?
|
|
99
|
+
Order.new('count', nil, 'desc')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def serieses
|
|
103
|
+
@serieses ||= Series.new_from_properties!(properties, context_record: context_record)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def report_options
|
|
107
|
+
report_options = properties[:report_options] || {}
|
|
108
|
+
head_rows_count = report_options[:head_rows_count]
|
|
109
|
+
foot_rows_count = report_options[:foot_rows_count]
|
|
110
|
+
foot_rows_count ||= report_options[:aggregations].count { |config| config[:from] == 'rows' } if report_options[:aggregations]
|
|
111
|
+
|
|
112
|
+
report_options[:head_rows_count] = head_rows_count if head_rows_count && head_rows_count > 0
|
|
113
|
+
report_options[:foot_rows_count] = foot_rows_count if foot_rows_count && foot_rows_count > 0
|
|
114
|
+
report_options.presence
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def data_format_method
|
|
118
|
+
ReportsKits.configuration.custom_method(report_options.try(:[], :data_format_method))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def csv_data_format_method
|
|
122
|
+
ReportsKits.configuration.custom_method(report_options.try(:[], :csv_data_format_method))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def format
|
|
126
|
+
properties[:format]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_csv_times(raw_data)
|
|
130
|
+
return raw_data unless primary_dimension.configured_by_time?
|
|
131
|
+
raw_data[:entities] = raw_data[:entities].map do |entity|
|
|
132
|
+
entity.label = Utils.format_csv_time(entity.instance)
|
|
133
|
+
entity
|
|
134
|
+
end
|
|
135
|
+
raw_data
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def primary_dimension
|
|
139
|
+
serieses.first.dimensions.first
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def table_or_csv?
|
|
143
|
+
format.in?(%w(table csv))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def inferred_options
|
|
147
|
+
return {} if has_data_method?
|
|
148
|
+
{
|
|
149
|
+
x_axis_label: primary_dimension.label,
|
|
150
|
+
y_axis_label: serieses.length == 1 ? serieses.first.label : nil
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class GenerateForProperties
|
|
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 composite_operator
|
|
16
|
+
raise ArgumentError.new('Aggregations require at least one series') if all_serieses.length == 0
|
|
17
|
+
dimension_keys_values = Data::AggregateComposite.new(properties, context_record: context_record).perform
|
|
18
|
+
serieses_dimension_keys_values = { CompositeSeries.new(properties, context_record: context_record) => dimension_keys_values }
|
|
19
|
+
elsif all_serieses.length == 1 && composite_serieses.length == 1
|
|
20
|
+
dimension_keys_values = Data::AggregateComposite.new(composite_serieses.first.properties, context_record: context_record).perform
|
|
21
|
+
serieses_dimension_keys_values = { all_serieses.first => dimension_keys_values }
|
|
22
|
+
elsif all_serieses.length == 1 && all_serieses.first.dimensions.length == 2
|
|
23
|
+
dimension_keys_values = Data::AggregateTwoDimensions.new(all_serieses.first).perform
|
|
24
|
+
serieses_dimension_keys_values = { all_serieses.first => dimension_keys_values }
|
|
25
|
+
serieses_dimension_keys_values = Data::PopulateTwoDimensions.new(serieses_dimension_keys_values).perform
|
|
26
|
+
elsif all_serieses.length > 0
|
|
27
|
+
serieses_dimension_keys_values = serieses_dimension_keys_values_for_one_dimension
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError.new('The configuration of measurse and dimensions is invalid')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
serieses_dimension_keys_values
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def composite_operator
|
|
38
|
+
properties[:composite_operator]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def name
|
|
42
|
+
properties[:name]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def serieses_dimension_keys_values_for_one_dimension
|
|
46
|
+
multi_dimension_serieses_exist = all_serieses.any? { |series| series.dimensions.length > 1 }
|
|
47
|
+
raise ArgumentError.new('When more than one series are configured, only one dimension may be used per series') if multi_dimension_serieses_exist
|
|
48
|
+
|
|
49
|
+
if all_serieses.length > 1 && ReportsKits.configuration.use_concurrent_queries
|
|
50
|
+
serieses_dimension_keys_values = multithreaded_serieses_dimension_keys_values
|
|
51
|
+
else
|
|
52
|
+
serieses_dimension_keys_values = all_serieses.map do |series|
|
|
53
|
+
[series, dimension_keys_values_for_series(series)]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
serieses_dimension_keys_values = Hash[serieses_dimension_keys_values]
|
|
57
|
+
Data::PopulateOneDimension.new(serieses_dimension_keys_values, context_record: context_record, properties: properties).perform
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def multithreaded_serieses_dimension_keys_values
|
|
61
|
+
threads = all_serieses.map do |series|
|
|
62
|
+
Thread.new do
|
|
63
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
|
64
|
+
begin
|
|
65
|
+
[series, dimension_keys_values_for_series(series)]
|
|
66
|
+
ensure
|
|
67
|
+
ActiveRecord::Base.connection.close if ActiveRecord::Base.connection
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
threads.map(&:join).map(&:value)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dimension_keys_values_for_series(series)
|
|
76
|
+
if series.is_a?(CompositeSeries)
|
|
77
|
+
Data::AggregateComposite.new(series.properties, context_record: context_record).perform
|
|
78
|
+
else
|
|
79
|
+
Data::AggregateOneDimension.new(series).perform
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def all_serieses
|
|
84
|
+
@all_serieses ||= Series.new_from_properties!(properties, context_record: context_record)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def composite_serieses
|
|
88
|
+
@composite_serieses ||= all_serieses.grep(CompositeSeries)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serieses
|
|
92
|
+
@serieses ||= all_serieses.grep(Series)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class NormalizeProperties
|
|
5
|
+
attr_accessor :raw_properties
|
|
6
|
+
|
|
7
|
+
def initialize(raw_properties)
|
|
8
|
+
self.raw_properties = raw_properties.dup
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def perform
|
|
12
|
+
context_properties = raw_properties.slice(:context_params, :contextual_filters)
|
|
13
|
+
properties = recursively_normalize_properties(raw_properties)
|
|
14
|
+
populate_context_properties(properties, context_properties: context_properties)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_filters(series_properties, ui_filters)
|
|
20
|
+
series_properties[:filters] = series_properties[:filters].map do |filter_properties|
|
|
21
|
+
filter_properties = { key: filter_properties } if filter_properties.is_a?(String)
|
|
22
|
+
key = filter_properties[:key]
|
|
23
|
+
ui_key = filter_properties[:ui_key]
|
|
24
|
+
value = ui_filters[key.to_sym]
|
|
25
|
+
value ||= ui_filters[ui_key.to_sym] if ui_key
|
|
26
|
+
if value
|
|
27
|
+
filter_properties[:criteria] ||= {}
|
|
28
|
+
filter_properties[:criteria][:value] = value
|
|
29
|
+
end
|
|
30
|
+
filter_properties
|
|
31
|
+
end
|
|
32
|
+
series_properties
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recursively_normalize_properties(properties, ui_filters: nil)
|
|
36
|
+
can_have_nesting = properties[:composite_operator].present? || properties[:series].is_a?(Array)
|
|
37
|
+
ui_filters ||= properties[:ui_filters]
|
|
38
|
+
properties[:series] ||= properties.slice(*Series::VALID_KEYS).presence
|
|
39
|
+
properties[:series] = [properties[:series]] if properties[:series].is_a?(Hash) && properties[:series].present?
|
|
40
|
+
return properties if ui_filters.blank? || properties[:series].blank?
|
|
41
|
+
properties[:series] = properties[:series].map do |series_properties|
|
|
42
|
+
series_properties = recursively_normalize_properties(series_properties, ui_filters: ui_filters) if can_have_nesting
|
|
43
|
+
next(series_properties) if series_properties[:filters].blank?
|
|
44
|
+
normalize_filters(series_properties, ui_filters)
|
|
45
|
+
end
|
|
46
|
+
properties
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def populate_context_properties(properties, context_properties: nil)
|
|
50
|
+
return properties if context_properties.blank? || properties.blank? || properties[:series].blank?
|
|
51
|
+
can_have_nesting = properties[:composite_operator].present? || properties[:series].is_a?(Array)
|
|
52
|
+
properties[:series] = properties[:series].map do |series_properties|
|
|
53
|
+
series_properties = series_properties.merge(context_properties)
|
|
54
|
+
series_properties = populate_context_properties(series_properties, context_properties: context_properties) if can_have_nesting
|
|
55
|
+
series_properties
|
|
56
|
+
end
|
|
57
|
+
properties
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class PopulateOneDimension
|
|
5
|
+
attr_accessor :sparse_serieses_dimension_keys_values, :context_record, :properties
|
|
6
|
+
|
|
7
|
+
def initialize(sparse_serieses_dimension_keys_values, context_record: nil, properties: nil)
|
|
8
|
+
self.sparse_serieses_dimension_keys_values = sparse_serieses_dimension_keys_values
|
|
9
|
+
self.context_record = context_record
|
|
10
|
+
self.properties = properties
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def perform
|
|
14
|
+
return sparse_serieses_dimension_keys_values if sparse_serieses_dimension_keys_values.length == 1
|
|
15
|
+
serieses_dimension_keys_values
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def serieses_dimension_keys_values
|
|
21
|
+
serieses_dimension_keys_values = sparse_serieses_dimension_keys_values.map do |series, dimension_keys_values|
|
|
22
|
+
dimension_keys.each do |key|
|
|
23
|
+
dimension_keys_values[key] ||= 0
|
|
24
|
+
end
|
|
25
|
+
[series, dimension_keys_values]
|
|
26
|
+
end
|
|
27
|
+
Hash[serieses_dimension_keys_values]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dimension_keys
|
|
31
|
+
dimension_keys_from_edit_dimension_keys_method || dimension_keys_from_results
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dimension_keys_from_edit_dimension_keys_method
|
|
35
|
+
return unless edit_dimension_keys_method
|
|
36
|
+
edit_dimension_keys_method.call(dimension_keys: dimension_keys_from_results, properties: properties, context_record: context_record)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dimension_keys_from_results
|
|
40
|
+
@dimension_keys_from_results ||= begin
|
|
41
|
+
sparse_serieses_dimension_keys_values.map do |series, dimension_keys_values|
|
|
42
|
+
dimension_keys_values.keys
|
|
43
|
+
end.reduce(&:+).uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def edit_dimension_keys_method
|
|
48
|
+
return unless properties
|
|
49
|
+
ReportsKits.configuration.custom_method(properties[:report_options].try(:[], :edit_dimension_keys_method))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class PopulateTwoDimensions
|
|
5
|
+
attr_accessor :serieses, :dimension, :second_dimension, :sparse_serieses_dimension_keys_values
|
|
6
|
+
|
|
7
|
+
def initialize(sparse_serieses_dimension_keys_values)
|
|
8
|
+
self.serieses = sparse_serieses_dimension_keys_values.keys
|
|
9
|
+
self.dimension = serieses.first.dimensions[0]
|
|
10
|
+
self.second_dimension = serieses.first.dimensions[1]
|
|
11
|
+
self.sparse_serieses_dimension_keys_values = sparse_serieses_dimension_keys_values
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform
|
|
15
|
+
serieses_populated_dimension_keys_values
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def serieses_populated_dimension_keys_values
|
|
21
|
+
serieses_dimension_keys_values = {}
|
|
22
|
+
secondary_keys_sums = Hash.new(0)
|
|
23
|
+
serieses_populated_primary_keys_secondary_keys_values.each do |series, primary_keys_secondary_keys_values|
|
|
24
|
+
primary_keys_secondary_keys_values.each do |primary_key, secondary_keys_values|
|
|
25
|
+
secondary_keys_values.each do |secondary_key, value|
|
|
26
|
+
secondary_keys_sums[secondary_key] += value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
sorted_secondary_keys = secondary_keys_sums.sort_by(&:last).reverse.map(&:first)
|
|
31
|
+
serieses_populated_primary_keys_secondary_keys_values.each do |series, primary_key_secondary_keys_values|
|
|
32
|
+
serieses_dimension_keys_values[series] = {}
|
|
33
|
+
primary_key_secondary_keys_values.each do |primary_key, secondary_keys_values|
|
|
34
|
+
secondary_keys_values = secondary_keys_values.sort_by { |key, _| sorted_secondary_keys.index(key) }
|
|
35
|
+
secondary_keys_values.each do |secondary_key, value|
|
|
36
|
+
dimension_key = [primary_key, secondary_key]
|
|
37
|
+
serieses_dimension_keys_values[series][dimension_key] = value
|
|
38
|
+
secondary_keys_sums[secondary_key] += value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
serieses_dimension_keys_values
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def serieses_populated_primary_keys_secondary_keys_values
|
|
46
|
+
@populated_dimension_keys_values ||= begin
|
|
47
|
+
serieses_populated_primary_keys_secondary_keys_values = {}
|
|
48
|
+
serieses.each do |series|
|
|
49
|
+
serieses_populated_primary_keys_secondary_keys_values[series] = {}
|
|
50
|
+
primary_keys.each do |primary_key|
|
|
51
|
+
serieses_populated_primary_keys_secondary_keys_values[series][primary_key] = {}
|
|
52
|
+
secondary_keys.each do |secondary_key|
|
|
53
|
+
value = serieses_primary_keys_secondary_keys_values[series][primary_key].try(:[], secondary_key) || 0
|
|
54
|
+
serieses_populated_primary_keys_secondary_keys_values[series][primary_key][secondary_key] = value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
serieses_populated_primary_keys_secondary_keys_values
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def serieses_primary_keys_secondary_keys_values
|
|
63
|
+
@serieses_primary_keys_secondary_keys_values ||= begin
|
|
64
|
+
serieses_primary_keys_secondary_keys_values = {}
|
|
65
|
+
sparse_serieses_dimension_keys_values.each do |series, dimension_keys_values|
|
|
66
|
+
serieses_primary_keys_secondary_keys_values[series] = {}
|
|
67
|
+
dimension_keys_values.each do |(primary_key, secondary_key), value|
|
|
68
|
+
primary_key = primary_key.to_date if primary_key.is_a?(Time)
|
|
69
|
+
secondary_key = secondary_key.to_date if secondary_key.is_a?(Time)
|
|
70
|
+
serieses_primary_keys_secondary_keys_values[series][primary_key] ||= {}
|
|
71
|
+
serieses_primary_keys_secondary_keys_values[series][primary_key][secondary_key] = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
serieses_primary_keys_secondary_keys_values
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dimension_keys
|
|
79
|
+
@dimension_keys ||= sparse_serieses_dimension_keys_values.values.map(&:keys).reduce(&:+).uniq
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def primary_keys
|
|
83
|
+
@primary_keys ||= begin
|
|
84
|
+
keys = Utils.populate_sparse_keys(dimension_keys.map(&:first).uniq, dimension: dimension)
|
|
85
|
+
unless dimension.configured_by_time?
|
|
86
|
+
limit = dimension.dimension_instances_limit
|
|
87
|
+
keys = keys.first(limit) if limit
|
|
88
|
+
end
|
|
89
|
+
keys
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def secondary_keys
|
|
94
|
+
@secondary_keys ||= begin
|
|
95
|
+
keys = Utils.populate_sparse_keys(dimension_keys.map(&:last).uniq, dimension: second_dimension)
|
|
96
|
+
limit = second_dimension.dimension_instances_limit
|
|
97
|
+
keys = keys.first(limit) if limit
|
|
98
|
+
keys
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class Utils
|
|
5
|
+
def self.format_configuration_time(time)
|
|
6
|
+
time.strftime('%b %-d, %Y')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.format_csv_time(time)
|
|
10
|
+
time.strftime('%Y-%m-%d')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.format_display_time(time)
|
|
14
|
+
time.strftime('%b %-d, \'%y')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.format_date_range(string)
|
|
18
|
+
start_at, end_at = parse_date_range(string, type: Array)
|
|
19
|
+
[format_display_time(start_at), FilterTypes::Datetime::SEPARATOR, format_display_time(end_at)].join(' ')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.format_time_value(value)
|
|
23
|
+
time = RelativeTime.parse(value, prevent_exceptions: true)
|
|
24
|
+
return value unless time
|
|
25
|
+
Utils.format_configuration_time(time)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.format_number(number)
|
|
29
|
+
number_i = number.to_i
|
|
30
|
+
return number_i if number == number_i
|
|
31
|
+
number.round(Generate::ROUND_PRECISION)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.quote_column_name(string)
|
|
35
|
+
ActiveRecord::Base.connection.quote_column_name(string)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse_date_string(string)
|
|
39
|
+
Date.parse(string)
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
RelativeTime.parse(string)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.parse_date_range(string, type: nil)
|
|
45
|
+
return if string.blank?
|
|
46
|
+
start_string, end_string = string.split(FilterTypes::Datetime::SEPARATOR)
|
|
47
|
+
start_at = parse_date_string(start_string)
|
|
48
|
+
end_at = parse_date_string(end_string)
|
|
49
|
+
if type == Array
|
|
50
|
+
[start_at, end_at]
|
|
51
|
+
else
|
|
52
|
+
(start_at..end_at)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.populate_sparse_hash(hash, dimension:)
|
|
57
|
+
keys = hash.keys
|
|
58
|
+
is_nested = dimension.series.has_two_dimensions?
|
|
59
|
+
if is_nested
|
|
60
|
+
keys_values = arrays_values_to_nested_hash(hash)
|
|
61
|
+
keys = keys_values.keys
|
|
62
|
+
else
|
|
63
|
+
keys_values = hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
first_key = dimension.first_key || keys.first
|
|
67
|
+
return hash unless first_key.is_a?(Time) || first_key.is_a?(Date)
|
|
68
|
+
keys_values = keys_values.map do |key, value|
|
|
69
|
+
key = key.to_date if key.is_a?(Time)
|
|
70
|
+
[key, value]
|
|
71
|
+
end.to_h
|
|
72
|
+
|
|
73
|
+
keys = populate_sparse_keys(keys, dimension: dimension)
|
|
74
|
+
populated_keys_values = {}
|
|
75
|
+
default_value = is_nested ? {} : 0
|
|
76
|
+
keys.each do |key|
|
|
77
|
+
populated_keys_values[key] = keys_values[key] || default_value
|
|
78
|
+
end
|
|
79
|
+
return nested_hash_to_arrays_values(populated_keys_values) if is_nested
|
|
80
|
+
populated_keys_values
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.populate_sparse_keys(keys, dimension:)
|
|
84
|
+
first_key = dimension.first_key || keys.first
|
|
85
|
+
return keys unless first_key.is_a?(Time) || first_key.is_a?(Date)
|
|
86
|
+
first_key = first_key.to_date
|
|
87
|
+
granularity = dimension.granularity
|
|
88
|
+
|
|
89
|
+
keys = keys.sort
|
|
90
|
+
last_key = (dimension.last_key || keys.last).to_date
|
|
91
|
+
if granularity == 'week'
|
|
92
|
+
first_key = first_key.beginning_of_week(ReportsKits.configuration.first_day_of_week)
|
|
93
|
+
last_key = last_key.beginning_of_week(ReportsKits.configuration.first_day_of_week)
|
|
94
|
+
elsif granularity == 'month'
|
|
95
|
+
first_key = first_key.beginning_of_month
|
|
96
|
+
last_key = last_key.beginning_of_month
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
date = first_key
|
|
100
|
+
populated_keys = []
|
|
101
|
+
interval = case granularity
|
|
102
|
+
when 'day' then 1.day
|
|
103
|
+
when 'month' then 1.month
|
|
104
|
+
else 1.week
|
|
105
|
+
end
|
|
106
|
+
loop do
|
|
107
|
+
populated_keys << date
|
|
108
|
+
break if date >= last_key
|
|
109
|
+
date += interval
|
|
110
|
+
end
|
|
111
|
+
populated_keys
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.arrays_values_to_nested_hash(arrays_values)
|
|
115
|
+
nested_hash = {}
|
|
116
|
+
arrays_values.each do |(key1, key2), value|
|
|
117
|
+
nested_hash[key1] ||= {}
|
|
118
|
+
nested_hash[key1][key2] ||= value
|
|
119
|
+
end
|
|
120
|
+
nested_hash
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.nested_hash_to_arrays_values(nested_hash)
|
|
124
|
+
arrays_values = {}
|
|
125
|
+
nested_hash.each do |key1, key2s_values|
|
|
126
|
+
if key2s_values.blank?
|
|
127
|
+
arrays_values[[key1, nil]] = 0
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
key2s_values.each do |key2, value|
|
|
131
|
+
arrays_values[[key1, key2]] = value
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
arrays_values
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
|
|
138
|
+
return nil unless dimension.instance_class
|
|
139
|
+
primary_key = dimension.instance_class.primary_key
|
|
140
|
+
dimension_instances = dimension.instance_class.where(primary_key => dimension_ids.uniq)
|
|
141
|
+
dimension_ids_dimension_instances = dimension_instances.map do |dimension_instance|
|
|
142
|
+
[dimension_instance.send(primary_key), dimension_instance]
|
|
143
|
+
end
|
|
144
|
+
Hash[dimension_ids_dimension_instances]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.dimension_key_to_entity(dimension_key, dimension, dimension_ids_dimension_instances)
|
|
148
|
+
instance = dimension_ids_dimension_instances ? dimension_ids_dimension_instances[dimension_key] : dimension_key
|
|
149
|
+
label = dimension_key_to_label(dimension_key, dimension, dimension_ids_dimension_instances)
|
|
150
|
+
Entity.new(dimension_key, label, instance)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.dimension_key_to_label(dimension_instance, dimension, ids_dimension_instances)
|
|
154
|
+
label = dimension.key_to_label(dimension_instance)
|
|
155
|
+
return label if label
|
|
156
|
+
return dimension_instance.to_s if dimension.configured_by_column? && dimension.column_type == :integer
|
|
157
|
+
case dimension_instance
|
|
158
|
+
when Time, Date
|
|
159
|
+
Utils.format_display_time(dimension_instance)
|
|
160
|
+
when Fixnum
|
|
161
|
+
raise ArgumentError.new("ids_dimension_instances must be present for Dimension with identifier: #{dimension_instance}") unless ids_dimension_instances
|
|
162
|
+
instance = ids_dimension_instances[dimension_instance.to_i]
|
|
163
|
+
return unless instance
|
|
164
|
+
instance.to_s
|
|
165
|
+
else
|
|
166
|
+
dimension_instance.to_s.gsub(/\.0$/, '')
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.raw_value_to_value(raw_value, value_format_method)
|
|
171
|
+
formatted_value = format_number(raw_value)
|
|
172
|
+
formatted_value = value_format_method.call(raw_value) if value_format_method
|
|
173
|
+
Value.new(raw_value, formatted_value)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
class Dimension
|
|
4
|
+
attr_accessor :properties
|
|
5
|
+
|
|
6
|
+
def initialize(properties)
|
|
7
|
+
raise ArgumentError.new('Blank properties') if properties.blank?
|
|
8
|
+
properties = { key: properties } if properties.is_a?(String)
|
|
9
|
+
raise ArgumentError.new("Dimension properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
|
|
10
|
+
properties = properties.deep_symbolize_keys
|
|
11
|
+
self.properties = properties
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def key
|
|
15
|
+
properties[:key]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def expression
|
|
19
|
+
properties[:expression] || key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def label
|
|
23
|
+
properties.key?(:label) ? properties[:label] : key.titleize
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|