reports_kit 0.2.0 → 0.3.0

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 (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?