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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rubocop.yml +85 -0
  4. data/.travis.yml +21 -0
  5. data/Appraisals +27 -0
  6. data/Gemfile +3 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +35 -0
  9. data/Rakefile +2 -0
  10. data/app/assets/javascripts/reports_kits/application.js +14 -0
  11. data/app/assets/javascripts/reports_kits/lib/_init.js +9 -0
  12. data/app/assets/javascripts/reports_kits/lib/chart.js +73 -0
  13. data/app/assets/javascripts/reports_kits/lib/report.js +135 -0
  14. data/app/assets/javascripts/reports_kits/lib/table.js +87 -0
  15. data/app/assets/javascripts/reports_kits/vendor/chart.js +12269 -0
  16. data/app/assets/javascripts/reports_kits/vendor/daterangepicker.js +1627 -0
  17. data/app/assets/javascripts/reports_kits/vendor/jquery.tablesorter.min.js +4 -0
  18. data/app/assets/javascripts/reports_kits/vendor/moment.js +4040 -0
  19. data/app/assets/javascripts/reports_kits/vendor/select2.full.js +6436 -0
  20. data/app/assets/stylesheets/reports_kits/application.css.scss +3 -0
  21. data/app/assets/stylesheets/reports_kits/reports.css.sass +33 -0
  22. data/app/assets/stylesheets/reports_kits/select2_overrides.css.sass +7 -0
  23. data/app/assets/stylesheets/reports_kits/vendor/daterangepicker.css +269 -0
  24. data/app/assets/stylesheets/reports_kits/vendor/select2-bootstrap.css +721 -0
  25. data/app/assets/stylesheets/reports_kits/vendor/select2.css +484 -0
  26. data/config/initializers/mime_types.rb +1 -0
  27. data/config/routes.rb +10 -0
  28. data/docs/images/demo.gif +0 -0
  29. data/docs/images/users_by_created_at.png +0 -0
  30. data/gemfiles/mysql.gemfile +7 -0
  31. data/gemfiles/mysql.gemfile.lock +167 -0
  32. data/gemfiles/postgresql.gemfile +7 -0
  33. data/gemfiles/postgresql.gemfile.lock +165 -0
  34. data/gemfiles/postgresql_rails_5.1.4.gemfile +8 -0
  35. data/gemfiles/postgresql_rails_5.1.4.gemfile.lock +168 -0
  36. data/gemfiles/rails_4_mysql.gemfile +8 -0
  37. data/gemfiles/rails_4_mysql.gemfile.lock +165 -0
  38. data/gemfiles/rails_4_postgresql.gemfile +8 -0
  39. data/gemfiles/rails_4_postgresql.gemfile.lock +163 -0
  40. data/gemfiles/rails_5.1.4_postgresql.gemfile +8 -0
  41. data/gemfiles/rails_5.1.4_postgresql.gemfile.lock +169 -0
  42. data/gemfiles/rails_5_mysql.gemfile +8 -0
  43. data/gemfiles/rails_5_mysql.gemfile.lock +171 -0
  44. data/gemfiles/rails_5_postgresql.gemfile +8 -0
  45. data/gemfiles/rails_5_postgresql.gemfile.lock +169 -0
  46. data/lib/reports_kits/base_controller.rb +17 -0
  47. data/lib/reports_kits/cache.rb +37 -0
  48. data/lib/reports_kits/configuration.rb +50 -0
  49. data/lib/reports_kits/engine.rb +21 -0
  50. data/lib/reports_kits/entity.rb +3 -0
  51. data/lib/reports_kits/filters_controller.rb +11 -0
  52. data/lib/reports_kits/form_builder.rb +66 -0
  53. data/lib/reports_kits/helper.rb +24 -0
  54. data/lib/reports_kits/model.rb +16 -0
  55. data/lib/reports_kits/model_configuration.rb +28 -0
  56. data/lib/reports_kits/normalized_params.rb +16 -0
  57. data/lib/reports_kits/order.rb +33 -0
  58. data/lib/reports_kits/relative_time.rb +42 -0
  59. data/lib/reports_kits/report_builder.rb +88 -0
  60. data/lib/reports_kits/reports/abstract_series.rb +9 -0
  61. data/lib/reports_kits/reports/adapters/mysql.rb +26 -0
  62. data/lib/reports_kits/reports/adapters/postgresql.rb +26 -0
  63. data/lib/reports_kits/reports/composite_series.rb +48 -0
  64. data/lib/reports_kits/reports/contextual_filter.rb +19 -0
  65. data/lib/reports_kits/reports/data/add_table_aggregations.rb +105 -0
  66. data/lib/reports_kits/reports/data/aggregate_composite.rb +97 -0
  67. data/lib/reports_kits/reports/data/aggregate_one_dimension.rb +39 -0
  68. data/lib/reports_kits/reports/data/aggregate_two_dimensions.rb +39 -0
  69. data/lib/reports_kits/reports/data/chart_data_for_data_method.rb +62 -0
  70. data/lib/reports_kits/reports/data/chart_options.rb +155 -0
  71. data/lib/reports_kits/reports/data/format_one_dimension.rb +140 -0
  72. data/lib/reports_kits/reports/data/format_table.rb +63 -0
  73. data/lib/reports_kits/reports/data/format_two_dimensions.rb +143 -0
  74. data/lib/reports_kits/reports/data/generate.rb +156 -0
  75. data/lib/reports_kits/reports/data/generate_for_properties.rb +97 -0
  76. data/lib/reports_kits/reports/data/normalize_properties.rb +62 -0
  77. data/lib/reports_kits/reports/data/populate_one_dimension.rb +54 -0
  78. data/lib/reports_kits/reports/data/populate_two_dimensions.rb +104 -0
  79. data/lib/reports_kits/reports/data/utils.rb +178 -0
  80. data/lib/reports_kits/reports/dimension.rb +27 -0
  81. data/lib/reports_kits/reports/dimension_with_series.rb +144 -0
  82. data/lib/reports_kits/reports/filter.rb +29 -0
  83. data/lib/reports_kits/reports/filter_types/base.rb +48 -0
  84. data/lib/reports_kits/reports/filter_types/boolean.rb +47 -0
  85. data/lib/reports_kits/reports/filter_types/datetime.rb +51 -0
  86. data/lib/reports_kits/reports/filter_types/number.rb +30 -0
  87. data/lib/reports_kits/reports/filter_types/records.rb +26 -0
  88. data/lib/reports_kits/reports/filter_types/string.rb +38 -0
  89. data/lib/reports_kits/reports/filter_with_series.rb +97 -0
  90. data/lib/reports_kits/reports/generate_autocomplete_method_results.rb +29 -0
  91. data/lib/reports_kits/reports/generate_autocomplete_results.rb +57 -0
  92. data/lib/reports_kits/reports/inferrable_configuration.rb +116 -0
  93. data/lib/reports_kits/reports/model_settings.rb +30 -0
  94. data/lib/reports_kits/reports/properties.rb +10 -0
  95. data/lib/reports_kits/reports/properties_to_filter.rb +40 -0
  96. data/lib/reports_kits/reports/series.rb +121 -0
  97. data/lib/reports_kits/reports_controller.rb +65 -0
  98. data/lib/reports_kits/utils.rb +11 -0
  99. data/lib/reports_kits/value.rb +3 -0
  100. data/lib/reports_kits/version.rb +3 -0
  101. data/lib/reports_kits.rb +79 -0
  102. data/reports_kits.gemspec +26 -0
  103. data/spec/factories/issue_factory.rb +4 -0
  104. data/spec/factories/issues_label_factory.rb +4 -0
  105. data/spec/factories/label_factory.rb +4 -0
  106. data/spec/factories/pro_repo_factory.rb +5 -0
  107. data/spec/factories/repo_factory.rb +5 -0
  108. data/spec/factories/tag_factory.rb +4 -0
  109. data/spec/fixtures/generate_inputs.yml +254 -0
  110. data/spec/fixtures/generate_outputs.yml +1216 -0
  111. data/spec/reports_kit/form_builder_spec.rb +26 -0
  112. data/spec/reports_kit/relative_time_spec.rb +29 -0
  113. data/spec/reports_kit/reports/data/generate/contextual_filters_spec.rb +60 -0
  114. data/spec/reports_kit/reports/data/generate_spec.rb +1371 -0
  115. data/spec/reports_kit/reports/data/normalize_properties_spec.rb +196 -0
  116. data/spec/reports_kit/reports/dimension_with_series_spec.rb +67 -0
  117. data/spec/reports_kit/reports/filter_with_series_spec.rb +39 -0
  118. data/spec/reports_kit/reports/generate_autocomplete_results_spec.rb +69 -0
  119. data/spec/spec_helper.rb +77 -0
  120. data/spec/support/config.rb +41 -0
  121. data/spec/support/example_data_methods.rb +25 -0
  122. data/spec/support/factory_girl.rb +5 -0
  123. data/spec/support/helpers.rb +25 -0
  124. data/spec/support/models/issue.rb +14 -0
  125. data/spec/support/models/issues_label.rb +4 -0
  126. data/spec/support/models/label.rb +5 -0
  127. data/spec/support/models/pro/repo.rb +5 -0
  128. data/spec/support/models/pro/special_issue.rb +4 -0
  129. data/spec/support/models/repo.rb +13 -0
  130. data/spec/support/models/tag.rb +4 -0
  131. data/spec/support/schema.rb +39 -0
  132. 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