reports_kits 0.7.5 → 0.7.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|