reports_kit 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.travis.yml +20 -0
  4. data/README.md +17 -19
  5. data/app/assets/javascripts/reports_kit/lib/_init.js +2 -1
  6. data/app/assets/javascripts/reports_kit/lib/chart.js +25 -16
  7. data/app/assets/javascripts/reports_kit/lib/report.js +42 -25
  8. data/app/assets/javascripts/reports_kit/lib/table.js +37 -2
  9. data/app/assets/stylesheets/reports_kit/reports.css.sass +3 -0
  10. data/docs/dimensions.md +26 -34
  11. data/docs/display_options.md +12 -15
  12. data/docs/filters.md +54 -63
  13. data/docs/measures.md +3 -4
  14. data/lib/reports_kit.rb +12 -10
  15. data/lib/reports_kit/base_controller.rb +1 -2
  16. data/lib/reports_kit/configuration.rb +19 -4
  17. data/lib/reports_kit/entity.rb +3 -0
  18. data/lib/reports_kit/helper.rb +17 -21
  19. data/lib/reports_kit/model_configuration.rb +1 -1
  20. data/lib/reports_kit/report_builder.rb +11 -11
  21. data/lib/reports_kit/reports/{abstract_measure.rb → abstract_series.rb} +1 -1
  22. data/lib/reports_kit/reports/{composite_measure.rb → composite_series.rb} +12 -8
  23. data/lib/reports_kit/reports/data/add_table_aggregations.rb +105 -0
  24. data/lib/reports_kit/reports/data/{composite_aggregation.rb → aggregate_composite.rb} +28 -27
  25. data/lib/reports_kit/reports/data/{one_dimension.rb → aggregate_one_dimension.rb} +9 -7
  26. data/lib/reports_kit/reports/data/{two_dimensions.rb → aggregate_two_dimensions.rb} +9 -8
  27. data/lib/reports_kit/reports/data/chart_options.rb +6 -11
  28. data/lib/reports_kit/reports/data/format_one_dimension.rb +56 -36
  29. data/lib/reports_kit/reports/data/format_table.rb +65 -0
  30. data/lib/reports_kit/reports/data/format_two_dimensions.rb +10 -8
  31. data/lib/reports_kit/reports/data/generate.rb +51 -28
  32. data/lib/reports_kit/reports/data/generate_for_properties.rb +52 -30
  33. data/lib/reports_kit/reports/data/populate_one_dimension.rb +30 -12
  34. data/lib/reports_kit/reports/data/populate_two_dimensions.rb +31 -31
  35. data/lib/reports_kit/reports/data/utils.rb +28 -24
  36. data/lib/reports_kit/reports/dimension.rb +4 -0
  37. data/lib/reports_kit/reports/{dimension_with_measure.rb → dimension_with_series.rb} +8 -9
  38. data/lib/reports_kit/reports/filter.rb +4 -0
  39. data/lib/reports_kit/reports/filter_types/base.rb +4 -3
  40. data/lib/reports_kit/reports/filter_types/boolean.rb +4 -4
  41. data/lib/reports_kit/reports/filter_types/datetime.rb +13 -3
  42. data/lib/reports_kit/reports/filter_types/number.rb +5 -5
  43. data/lib/reports_kit/reports/{filter_with_measure.rb → filter_with_series.rb} +7 -7
  44. data/lib/reports_kit/reports/generate_autocomplete_results.rb +2 -2
  45. data/lib/reports_kit/reports/inferrable_configuration.rb +6 -6
  46. data/lib/reports_kit/reports/{measure.rb → series.rb} +28 -15
  47. data/lib/reports_kit/reports_controller.rb +25 -5
  48. data/lib/reports_kit/value.rb +3 -0
  49. data/lib/reports_kit/version.rb +1 -1
  50. data/spec/fixtures/generate_inputs.yml +116 -63
  51. data/spec/fixtures/generate_outputs.yml +64 -0
  52. data/spec/reports_kit/report_builder_spec.rb +10 -12
  53. data/spec/reports_kit/reports/data/generate_spec.rb +559 -140
  54. data/spec/reports_kit/reports/{dimension_with_measure_spec.rb → dimension_with_series_spec.rb} +5 -7
  55. data/spec/reports_kit/reports/{filter_with_measure_spec.rb → filter_with_series_spec.rb} +3 -3
  56. data/spec/spec_helper.rb +5 -5
  57. data/spec/support/config.rb +31 -1
  58. data/spec/support/helpers.rb +6 -2
  59. metadata +17 -14
  60. data/lib/reports_kit/reports/data/entity.rb +0 -7
  61. data/lib/reports_kit/reports/data/value.rb +0 -7
@@ -11,13 +11,13 @@ module ReportsKit
11
11
  self.additional_params = additional_params
12
12
  end
13
13
 
14
- def check_box(filter_key, options={})
14
+ def check_box(filter_key, options = {})
15
15
  filter = validate_filter!(filter_key)
16
16
  checked = filter.normalized_properties[:criteria][:value] == 'true'
17
17
  check_box_tag(filter_key, '1', checked, options)
18
18
  end
19
19
 
20
- def date_range(filter_key, options={})
20
+ def date_range(filter_key, options = {})
21
21
  filter = validate_filter!(filter_key)
22
22
  defaults = { class: 'form-control input-sm date_range_picker' }
23
23
  options = defaults.deep_merge(options)
@@ -26,11 +26,11 @@ module ReportsKit
26
26
  text_field_tag(filter_key, value, options)
27
27
  end
28
28
 
29
- def multi_autocomplete(filter_key, options={})
29
+ def multi_autocomplete(filter_key, options = {})
30
30
  validate_filter!(filter_key)
31
- filter = measure_filters.find { |f| f.key == filter_key.to_s }
31
+ filter = series_filters.find { |f| f.key == filter_key.to_s }
32
32
  reports_kit_path = Rails.application.routes.url_helpers.reports_kit_path
33
- path = "#{reports_kit_path}reports_kit/resources/measures/#{filter.measure.key}/filters/#{filter_key}/autocomplete?"
33
+ path = "#{reports_kit_path}reports_kit/resources/measures/#{filter.series.key}/filters/#{filter_key}/autocomplete?"
34
34
  path += additional_params.to_query if additional_params.present?
35
35
  scope = options.delete(:scope)
36
36
  params = {}
@@ -49,7 +49,7 @@ module ReportsKit
49
49
  select_tag(filter_key, nil, options)
50
50
  end
51
51
 
52
- def string_filter(filter_key, options={})
52
+ def string_filter(filter_key, options = {})
53
53
  filter = validate_filter!(filter_key)
54
54
  defaults = { class: 'form-control input-sm' }
55
55
  options = defaults.deep_merge(options)
@@ -66,11 +66,11 @@ module ReportsKit
66
66
  end
67
67
 
68
68
  def filters
69
- ui_filters + measure_filters
69
+ ui_filters + series_filters
70
70
  end
71
71
 
72
- def measure_filters
73
- measures.map(&:filters).flatten
72
+ def series_filters
73
+ serieses.map(&:filters).flatten
74
74
  end
75
75
 
76
76
  def ui_filters
@@ -80,8 +80,8 @@ module ReportsKit
80
80
  end
81
81
  end
82
82
 
83
- def measures
84
- Reports::Measure.new_from_properties!(properties, context_record: nil)
83
+ def serieses
84
+ Reports::Series.new_from_properties!(properties, context_record: nil)
85
85
  end
86
86
 
87
87
  def default_date_range_value
@@ -1,6 +1,6 @@
1
1
  module ReportsKit
2
2
  module Reports
3
- class AbstractMeasure
3
+ class AbstractSeries
4
4
  def value_format_method
5
5
  ReportsKit.configuration.custom_method(properties[:value_format_method])
6
6
  end
@@ -1,6 +1,6 @@
1
1
  module ReportsKit
2
2
  module Reports
3
- class CompositeMeasure < AbstractMeasure
3
+ class CompositeSeries < AbstractSeries
4
4
  attr_accessor :properties
5
5
 
6
6
  def initialize(properties)
@@ -19,24 +19,28 @@ module ReportsKit
19
19
  properties[:composite_operator]
20
20
  end
21
21
 
22
- def measures
23
- @measures ||= Reports::Measure.new_from_properties!(properties, context_record: nil)
22
+ def limit
23
+ properties[:limit]
24
+ end
25
+
26
+ def serieses
27
+ @serieses ||= Reports::Series.new_from_properties!(properties, context_record: nil)
24
28
  end
25
29
 
26
30
  def filters
27
- measures.map(&:filters).flatten
31
+ serieses.map(&:filters).flatten
28
32
  end
29
33
 
30
- def primary_measure
31
- measures.first
34
+ def primary_series
35
+ serieses.first
32
36
  end
33
37
 
34
38
  def dimensions
35
- primary_measure.dimensions
39
+ primary_series.dimensions
36
40
  end
37
41
 
38
42
  def model_class
39
- primary_measure.model_class
43
+ primary_series.model_class
40
44
  end
41
45
  end
42
46
  end
@@ -0,0 +1,105 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class AddTableAggregations
5
+ attr_accessor :data, :report_options
6
+
7
+ VALID_AGGREGATION_OPERATORS = [:sum]
8
+
9
+ def initialize(data, report_options:)
10
+ self.data = data
11
+ self.report_options = report_options || {}
12
+ end
13
+
14
+ def perform
15
+ data_with_aggregations
16
+ end
17
+
18
+ private
19
+
20
+ def data_with_aggregations
21
+ return data if row_aggregation_configs.blank? && column_aggregation_configs.blank?
22
+ {
23
+ entities: entities,
24
+ datasets: datasets
25
+ }
26
+ end
27
+
28
+ def entities
29
+ column_aggregation_entities = column_aggregation_configs.map do |config|
30
+ ReportsKit::Entity.new(config[:label], config[:label], config[:label])
31
+ end
32
+ entities_with_row_aggregations + column_aggregation_entities
33
+ end
34
+
35
+ def datasets
36
+ datasets_with_row_aggregations.map do |dataset|
37
+ column_aggregation_configs.each do |config|
38
+ value = aggregate_array(dataset[:values].map(&:formatted), config[:operator])
39
+ dataset[:values] << ReportsKit::Value.new(value, value)
40
+ end
41
+ dataset
42
+ end
43
+ end
44
+
45
+ def entities_with_row_aggregations
46
+ @entities_with_row_aggregations ||= begin
47
+ return original_entities if row_aggregation_configs.blank?
48
+ row_aggregation_entities = row_aggregation_configs.map do |config|
49
+ ReportsKit::Entity.new(config[:label], config[:label], config[:label])
50
+ end
51
+ original_entities + row_aggregation_entities
52
+ end
53
+ end
54
+
55
+ def datasets_with_row_aggregations
56
+ @datasets_with_row_aggregations ||= begin
57
+ return original_datasets if row_aggregation_configs.blank?
58
+ row_aggregation_datasets = row_aggregation_configs.map do |config|
59
+ values = original_datasets.map { |dataset| dataset[:values].map(&:formatted) }.transpose
60
+ aggregated_values = aggregate_array_of_arrays(values, config[:operator])
61
+ values = aggregated_values.map { |value| ReportsKit::Value.new(value, value) }
62
+ {
63
+ entity: ReportsKit::Entity.new(config[:label], config[:label], config[:label]),
64
+ values: values
65
+ }
66
+ end
67
+ original_datasets + row_aggregation_datasets
68
+ end
69
+ end
70
+
71
+ def original_entities
72
+ data[:entities]
73
+ end
74
+
75
+ def original_datasets
76
+ data[:datasets]
77
+ end
78
+
79
+ def row_aggregation_configs
80
+ return [] if report_options[:aggregations].blank?
81
+ report_options[:aggregations].select { |config| config[:from] == 'rows' }
82
+ end
83
+
84
+ def column_aggregation_configs
85
+ return [] if report_options[:aggregations].blank?
86
+ report_options[:aggregations].select { |config| config[:from] == 'columns' }
87
+ end
88
+
89
+ def aggregate_array(values, operator)
90
+ operator = operator.try(:to_sym)
91
+ raise ArgumentError.new("Invalid aggregation operator: #{operator}") unless operator.in?(VALID_AGGREGATION_OPERATORS)
92
+ if values.first.is_a?(Numeric)
93
+ values.public_send(operator)
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ def aggregate_array_of_arrays(array_of_arrays, operator)
100
+ array_of_arrays.map { |values| aggregate_array(values, operator) }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,8 +1,10 @@
1
1
  module ReportsKit
2
2
  module Reports
3
3
  module Data
4
- class CompositeAggregation
5
- attr_accessor :composite_operator, :properties, :context_record
4
+ class AggregateComposite
5
+ attr_accessor :composite_series, :context_record
6
+
7
+ delegate :composite_operator, :properties, to: :composite_series
6
8
 
7
9
  OPERATORS_METHODS = {
8
10
  '+' => :+,
@@ -10,7 +12,7 @@ module ReportsKit
10
12
  '*' => :*,
11
13
  '/' => :/,
12
14
  '%' => -> (values) {
13
- raise ArgumentError.new("Percentage composite aggregations must have exactly two measures") if values.length != 2
15
+ raise ArgumentError.new('Percentage composite aggregations must have exactly two series') if values.length != 2
14
16
  numerator, denominator = values
15
17
  return 0 if denominator == 0
16
18
  ((numerator.to_f / denominator) * 100).round(1)
@@ -18,43 +20,42 @@ module ReportsKit
18
20
  }
19
21
 
20
22
  def initialize(properties, context_record:)
21
- self.properties = properties
22
- self.composite_operator = properties[:composite_operator]
23
+ self.composite_series = CompositeSeries.new(properties)
23
24
  self.context_record = context_record
24
25
  end
25
26
 
26
27
  def perform
27
- return measures_results_for_one_dimension if dimension_count == 1
28
- return measures_results_for_two_dimensions if dimension_count == 2
29
- raise ArgumentError.new("Composite aggregations' measures can only have 1-2 dimensions")
28
+ return serieses_results_for_one_dimension if dimension_count == 1
29
+ return serieses_results_for_two_dimensions if dimension_count == 2
30
+ raise ArgumentError.new("Composite aggregations' series can only have 1-2 dimensions")
30
31
  end
31
32
 
32
33
  private
33
34
 
34
- def measures_results_for_one_dimension
35
- measures_results = Hash[measures.map { |measure| [measure, OneDimension.new(measure).perform] }]
36
- measures_results = Data::PopulateOneDimension.new(measures_results).perform
37
- sorted_dimension_keys_values = sort_dimension_keys_values(measures_results)
35
+ def serieses_results_for_one_dimension
36
+ serieses_results = Hash[serieses.map { |series| [series, AggregateOneDimension.new(series).perform] }]
37
+ serieses_results = Data::PopulateOneDimension.new(serieses_results, context_record: context_record, properties: properties).perform
38
+ sorted_dimension_keys_values = sort_dimension_keys_values(serieses_results)
38
39
  value_lists = sorted_dimension_keys_values.map(&:values)
39
40
  composited_values = value_lists.transpose.map { |data| reduce(data) }
40
41
  dimension_keys = sorted_dimension_keys_values.first.keys
41
- composited_keys_values = Hash[dimension_keys.zip(composited_values)]
42
- composited_keys_values
42
+ composited_keys_values = dimension_keys.zip(composited_values)
43
+ Hash[composited_keys_values]
43
44
  end
44
45
 
45
- def measures_results_for_two_dimensions
46
- measures_results = Hash[measures.map { |measure| [measure, TwoDimensions.new(measure).perform] }]
47
- measures_results = Data::PopulateTwoDimensions.new(measures_results).perform
48
- value_lists = measures_results.values.map(&:values)
46
+ def serieses_results_for_two_dimensions
47
+ serieses_results = Hash[serieses.map { |series| [series, AggregateTwoDimensions.new(series).perform] }]
48
+ serieses_results = Data::PopulateTwoDimensions.new(serieses_results).perform
49
+ value_lists = serieses_results.values.map(&:values)
49
50
  composited_values = value_lists.transpose.map { |data| reduce(data) }
50
- dimension_keys = measures_results.values.first.keys
51
- composited_keys_values = Hash[dimension_keys.zip(composited_values)]
52
- composited_keys_values
51
+ dimension_keys = serieses_results.values.first.keys
52
+ composited_keys_values = dimension_keys.zip(composited_values)
53
+ Hash[composited_keys_values]
53
54
  end
54
55
 
55
56
  # Before performing a composition of values, we need to make sure that the values are sorted by the same dimension key.
56
- def sort_dimension_keys_values(measures_results)
57
- dimension_keys_values_list = measures_results.values
57
+ def sort_dimension_keys_values(serieses_results)
58
+ dimension_keys_values_list = serieses_results.values
58
59
  sorted_dimension_keys_values = dimension_keys_values_list.map do |dimension_keys_values|
59
60
  dimension_keys_values = dimension_keys_values.sort_by do |dimension_key, value|
60
61
  is_boolean = dimension_key.is_a?(TrueClass) || dimension_key.is_a?(FalseClass)
@@ -82,13 +83,13 @@ module ReportsKit
82
83
  end
83
84
 
84
85
  def dimension_count
85
- unique_dimension_counts = measures.map { |measure| measure.dimensions.length }.uniq
86
- raise ArgumentError.new('All measures must have the same number of dimensions') if unique_dimension_counts.length > 1
86
+ unique_dimension_counts = serieses.map { |series| series.dimensions.length }.uniq
87
+ raise ArgumentError.new('All series must have the same number of dimensions') if unique_dimension_counts.length > 1
87
88
  unique_dimension_counts.first
88
89
  end
89
90
 
90
- def measures
91
- @measures ||= Measure.new_from_properties!(properties, context_record: context_record)
91
+ def serieses
92
+ @serieses ||= Series.new_from_properties!(properties, context_record: context_record)
92
93
  end
93
94
  end
94
95
  end
@@ -1,12 +1,12 @@
1
1
  module ReportsKit
2
2
  module Reports
3
3
  module Data
4
- class OneDimension
5
- attr_accessor :measure, :dimension
4
+ class AggregateOneDimension
5
+ attr_accessor :series, :dimension
6
6
 
7
- def initialize(measure)
8
- self.measure = measure
9
- self.dimension = measure.dimensions[0]
7
+ def initialize(series)
8
+ self.series = series
9
+ self.dimension = series.dimensions[0]
10
10
  end
11
11
 
12
12
  def perform
@@ -16,15 +16,17 @@ module ReportsKit
16
16
  private
17
17
 
18
18
  def dimension_keys_values
19
- relation = measure.filtered_relation
19
+ relation = series.filtered_relation
20
20
  relation = relation.group(dimension.group_expression)
21
21
  relation = relation.joins(dimension.joins) if dimension.joins
22
22
  relation = relation.limit(dimension.dimension_instances_limit) if dimension.dimension_instances_limit
23
23
  relation = relation.order(order)
24
- dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
24
+ relation = series.edit_relation_method.call(relation) if series.edit_relation_method
25
+ dimension_keys_values = relation.distinct.public_send(*series.aggregate_function)
25
26
  dimension_keys_values = Utils.populate_sparse_hash(dimension_keys_values, dimension: dimension)
26
27
  dimension_keys_values.delete(nil)
27
28
  dimension_keys_values.delete('')
29
+ dimension_keys_values = dimension_keys_values.take(series.limit) if series.limit
28
30
  Hash[dimension_keys_values]
29
31
  end
30
32
 
@@ -1,13 +1,13 @@
1
1
  module ReportsKit
2
2
  module Reports
3
3
  module Data
4
- class TwoDimensions
5
- attr_accessor :measure, :dimension, :second_dimension
4
+ class AggregateTwoDimensions
5
+ attr_accessor :series, :dimension, :second_dimension
6
6
 
7
- def initialize(measure)
8
- self.measure = measure
9
- self.dimension = measure.dimensions[0]
10
- self.second_dimension = measure.dimensions[1]
7
+ def initialize(series)
8
+ self.series = series
9
+ self.dimension = series.dimensions[0]
10
+ self.second_dimension = series.dimensions[1]
11
11
  end
12
12
 
13
13
  def perform
@@ -17,12 +17,13 @@ module ReportsKit
17
17
  private
18
18
 
19
19
  def dimension_keys_values
20
- relation = measure.filtered_relation
20
+ relation = series.filtered_relation
21
21
  relation = relation.group(dimension.group_expression, second_dimension.group_expression)
22
22
  relation = relation.joins(dimension.joins) if dimension.joins
23
23
  relation = relation.joins(second_dimension.joins) if second_dimension.joins
24
24
  relation = relation.order(order)
25
- dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
25
+ relation = series.edit_relation_method.call(relation) if series.edit_relation_method
26
+ dimension_keys_values = relation.distinct.public_send(*series.aggregate_function)
26
27
  dimension_keys_values = Utils.populate_sparse_hash(dimension_keys_values, dimension: dimension)
27
28
  dimension_keys_values.delete(nil)
28
29
  dimension_keys_values.delete('')
@@ -67,7 +67,6 @@ module ReportsKit
67
67
  set_colors
68
68
  set_chart_options
69
69
  set_dataset_options
70
- set_standard_options
71
70
  set_type
72
71
  data
73
72
  end
@@ -83,7 +82,7 @@ module ReportsKit
83
82
  end
84
83
 
85
84
  def set_record_scoped_colors
86
- self.data[:chart_data][:datasets] = self.data[:chart_data][:datasets].map do |dataset|
85
+ data[:chart_data][:datasets] = data[:chart_data][:datasets].map do |dataset|
87
86
  length = dataset[:data].length
88
87
  dataset[:backgroundColor] = DEFAULT_COLORS * (length.to_f / DEFAULT_COLORS.length).ceil
89
88
  dataset
@@ -91,7 +90,7 @@ module ReportsKit
91
90
  end
92
91
 
93
92
  def set_dataset_scoped_colors
94
- self.data[:chart_data][:datasets] = data[:chart_data][:datasets].map.with_index do |dataset, index|
93
+ data[:chart_data][:datasets] = data[:chart_data][:datasets].map.with_index do |dataset, index|
95
94
  color = DEFAULT_COLORS[index % DEFAULT_COLORS.length]
96
95
  dataset[:backgroundColor] = color
97
96
  dataset[:borderColor] = color
@@ -132,23 +131,19 @@ module ReportsKit
132
131
  def set_chart_options
133
132
  merged_options = default_options
134
133
  merged_options = merged_options.deep_merge(chart_options) if chart_options
135
- self.data[:chart_data][:options] = merged_options
134
+ data[:chart_data][:options] = merged_options
136
135
  end
137
136
 
138
137
  def set_dataset_options
139
- return if self.data[:chart_data][:datasets].blank? || dataset_options.blank?
140
- self.data[:chart_data][:datasets] = self.data[:chart_data][:datasets].map do |dataset|
138
+ return if data[:chart_data][:datasets].blank? || dataset_options.blank?
139
+ data[:chart_data][:datasets] = data[:chart_data][:datasets].map do |dataset|
141
140
  dataset.merge(dataset_options)
142
141
  end
143
142
  end
144
143
 
145
- def set_standard_options
146
- self.data[:chart_data][:standard_options] = options[:standard_options] if options[:standard_options].present?
147
- end
148
-
149
144
  def set_type
150
145
  return if type.blank?
151
- self.data[:type] = type
146
+ data[:type] = type
152
147
  end
153
148
 
154
149
  def donut_or_pie_chart?