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.
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