reports_kit 0.1.0 → 0.2.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -5
  3. data/app/assets/javascripts/reports_kit/lib/chart.js +33 -8
  4. data/app/assets/javascripts/reports_kit/lib/report.js +27 -26
  5. data/app/assets/javascripts/reports_kit/lib/table.js +58 -0
  6. data/app/assets/javascripts/reports_kit/vendor/jquery.tablesorter.min.js +4 -0
  7. data/app/assets/stylesheets/reports_kit/reports.css.sass +20 -0
  8. data/config/initializers/mime_types.rb +1 -0
  9. data/docs/dimensions.md +34 -26
  10. data/docs/display_options.md +15 -12
  11. data/docs/filters.md +2 -2
  12. data/docs/measures.md +4 -3
  13. data/gemfiles/mysql.gemfile.lock +14 -1
  14. data/gemfiles/postgresql.gemfile.lock +14 -1
  15. data/lib/reports_kit.rb +15 -0
  16. data/lib/reports_kit/cache.rb +37 -0
  17. data/lib/reports_kit/configuration.rb +13 -1
  18. data/lib/reports_kit/helper.rb +54 -3
  19. data/lib/reports_kit/model_configuration.rb +6 -1
  20. data/lib/reports_kit/order.rb +33 -0
  21. data/lib/reports_kit/relative_time.rb +42 -0
  22. data/lib/reports_kit/report_builder.rb +34 -15
  23. data/lib/reports_kit/reports/abstract_measure.rb +9 -0
  24. data/lib/reports_kit/reports/adapters/mysql.rb +8 -1
  25. data/lib/reports_kit/reports/adapters/postgresql.rb +8 -1
  26. data/lib/reports_kit/reports/composite_measure.rb +43 -0
  27. data/lib/reports_kit/reports/data/chart_options.rb +5 -0
  28. data/lib/reports_kit/reports/data/composite_aggregation.rb +96 -0
  29. data/lib/reports_kit/reports/data/entity.rb +7 -0
  30. data/lib/reports_kit/reports/data/format_one_dimension.rb +120 -0
  31. data/lib/reports_kit/reports/data/format_two_dimensions.rb +141 -0
  32. data/lib/reports_kit/reports/data/generate.rb +72 -25
  33. data/lib/reports_kit/reports/data/generate_for_properties.rb +75 -0
  34. data/lib/reports_kit/reports/data/one_dimension.rb +15 -49
  35. data/lib/reports_kit/reports/data/populate_one_dimension.rb +36 -0
  36. data/lib/reports_kit/reports/data/populate_two_dimensions.rb +104 -0
  37. data/lib/reports_kit/reports/data/two_dimensions.rb +15 -110
  38. data/lib/reports_kit/reports/data/utils.rb +77 -12
  39. data/lib/reports_kit/reports/data/value.rb +7 -0
  40. data/lib/reports_kit/reports/dimension.rb +4 -110
  41. data/lib/reports_kit/reports/dimension_with_measure.rb +137 -0
  42. data/lib/reports_kit/reports/filter.rb +5 -64
  43. data/lib/reports_kit/reports/filter_types/base.rb +1 -1
  44. data/lib/reports_kit/reports/filter_types/boolean.rb +9 -7
  45. data/lib/reports_kit/reports/filter_types/datetime.rb +7 -5
  46. data/lib/reports_kit/reports/filter_types/number.rb +2 -0
  47. data/lib/reports_kit/reports/filter_with_measure.rb +84 -0
  48. data/lib/reports_kit/reports/generate_autocomplete_results.rb +1 -1
  49. data/lib/reports_kit/reports/inferrable_configuration.rb +32 -13
  50. data/lib/reports_kit/reports/measure.rb +48 -12
  51. data/lib/reports_kit/reports_controller.rb +42 -3
  52. data/lib/reports_kit/version.rb +1 -1
  53. data/reports_kit.gemspec +2 -0
  54. data/spec/fixtures/generate_inputs.yml +146 -21
  55. data/spec/fixtures/generate_outputs.yml +768 -17
  56. data/spec/reports_kit/relative_time_spec.rb +29 -0
  57. data/spec/reports_kit/report_builder_spec.rb +28 -0
  58. data/spec/reports_kit/reports/data/generate_spec.rb +614 -27
  59. data/spec/reports_kit/reports/dimension_with_measure_spec.rb +69 -0
  60. data/spec/reports_kit/reports/{filter_spec.rb → filter_with_measure_spec.rb} +4 -3
  61. data/spec/spec_helper.rb +7 -2
  62. data/spec/support/config.rb +11 -0
  63. data/spec/support/helpers.rb +3 -3
  64. data/spec/support/models/issue.rb +7 -0
  65. metadata +53 -4
  66. data/spec/reports_kit/reports/dimension_spec.rb +0 -54
@@ -0,0 +1,9 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class AbstractMeasure
4
+ def value_format_method
5
+ ReportsKit.configuration.custom_method(properties[:value_format_method])
6
+ end
7
+ end
8
+ end
9
+ end
@@ -7,7 +7,14 @@ module ReportsKit
7
7
  end
8
8
 
9
9
  def self.truncate_to_week(column)
10
- "DATE_SUB(DATE(#{column}), INTERVAL DAYOFWEEK(#{column}) - 2 DAY)"
10
+ case ReportsKit.configuration.first_day_of_week
11
+ when :sunday
12
+ "DATE_SUB(DATE(#{column}), INTERVAL DAYOFWEEK(#{column}) - 1 DAY)"
13
+ when :monday
14
+ "DATE_SUB(DATE(#{column}), INTERVAL DAYOFWEEK(#{column}) - 2 DAY)"
15
+ else
16
+ raise ArgumentError.new("Unsupported first_day_of_week: #{ReportsKit.configuration.first_day_of_week}")
17
+ end
11
18
  end
12
19
  end
13
20
  end
@@ -7,7 +7,14 @@ module ReportsKit
7
7
  end
8
8
 
9
9
  def self.truncate_to_week(column)
10
- "DATE_TRUNC('week', #{column}::timestamp)"
10
+ case ReportsKit.configuration.first_day_of_week
11
+ when :sunday
12
+ "DATE_TRUNC('week', #{column}::timestamp + '1 day'::interval) - '1 day'::interval"
13
+ when :monday
14
+ "DATE_TRUNC('week', #{column}::timestamp)"
15
+ else
16
+ raise ArgumentError.new("Unsupported first_day_of_week: #{ReportsKit.configuration.first_day_of_week}")
17
+ end
11
18
  end
12
19
  end
13
20
  end
@@ -0,0 +1,43 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class CompositeMeasure < AbstractMeasure
4
+ attr_accessor :properties
5
+
6
+ def initialize(properties)
7
+ self.properties = properties.dup
8
+ end
9
+
10
+ def label
11
+ name
12
+ end
13
+
14
+ def name
15
+ properties[:name]
16
+ end
17
+
18
+ def composite_operator
19
+ properties[:composite_operator]
20
+ end
21
+
22
+ def measures
23
+ @measures ||= Reports::Measure.new_from_properties!(properties, context_record: nil)
24
+ end
25
+
26
+ def filters
27
+ measures.map(&:filters).flatten
28
+ end
29
+
30
+ def primary_measure
31
+ measures.first
32
+ end
33
+
34
+ def dimensions
35
+ primary_measure.dimensions
36
+ end
37
+
38
+ def model_class
39
+ primary_measure.model_class
40
+ end
41
+ end
42
+ end
43
+ end
@@ -67,6 +67,7 @@ module ReportsKit
67
67
  set_colors
68
68
  set_chart_options
69
69
  set_dataset_options
70
+ set_standard_options
70
71
  set_type
71
72
  data
72
73
  end
@@ -141,6 +142,10 @@ module ReportsKit
141
142
  end
142
143
  end
143
144
 
145
+ def set_standard_options
146
+ self.data[:chart_data][:standard_options] = options[:standard_options] if options[:standard_options].present?
147
+ end
148
+
144
149
  def set_type
145
150
  return if type.blank?
146
151
  self.data[:type] = type
@@ -0,0 +1,96 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class CompositeAggregation
5
+ attr_accessor :composite_operator, :properties, :context_record
6
+
7
+ OPERATORS_METHODS = {
8
+ '+' => :+,
9
+ '-' => :-,
10
+ '*' => :*,
11
+ '/' => :/,
12
+ '%' => -> (values) {
13
+ raise ArgumentError.new("Percentage composite aggregations must have exactly two measures") if values.length != 2
14
+ numerator, denominator = values
15
+ return 0 if denominator == 0
16
+ ((numerator.to_f / denominator) * 100).round(1)
17
+ }
18
+ }
19
+
20
+ def initialize(properties, context_record:)
21
+ self.properties = properties
22
+ self.composite_operator = properties[:composite_operator]
23
+ self.context_record = context_record
24
+ end
25
+
26
+ 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")
30
+ end
31
+
32
+ private
33
+
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)
38
+ value_lists = sorted_dimension_keys_values.map(&:values)
39
+ composited_values = value_lists.transpose.map { |data| reduce(data) }
40
+ dimension_keys = sorted_dimension_keys_values.first.keys
41
+ composited_keys_values = Hash[dimension_keys.zip(composited_values)]
42
+ composited_keys_values
43
+ end
44
+
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)
49
+ 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
53
+ end
54
+
55
+ # 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
58
+ sorted_dimension_keys_values = dimension_keys_values_list.map do |dimension_keys_values|
59
+ dimension_keys_values = dimension_keys_values.sort_by do |dimension_key, value|
60
+ is_boolean = dimension_key.is_a?(TrueClass) || dimension_key.is_a?(FalseClass)
61
+ is_boolean ? (dimension_key ? 0 : 1) : dimension_key
62
+ end
63
+ Hash[dimension_keys_values]
64
+ end
65
+ sorted_dimension_keys_values
66
+ end
67
+
68
+ def reduce(values)
69
+ if composite_method.is_a?(Symbol)
70
+ values.reduce(&composite_method)
71
+ elsif composite_method.is_a?(Proc)
72
+ values = composite_method.call(values)
73
+ else
74
+ raise ArgumentError.new("Invalid composite method type: #{composite_method.class}")
75
+ end
76
+ end
77
+
78
+ def composite_method
79
+ composite_method = OPERATORS_METHODS[composite_operator]
80
+ raise ArgumentError.new("Invalid composite_operator: #{composite_operator}") unless composite_method
81
+ composite_method
82
+ end
83
+
84
+ 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
87
+ unique_dimension_counts.first
88
+ end
89
+
90
+ def measures
91
+ @measures ||= Measure.new_from_properties!(properties, context_record: context_record)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,7 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ Entity = Struct.new(:key, :label, :instance)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,120 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class FormatOneDimension
5
+ attr_accessor :measures_results, :measures, :order
6
+
7
+ def initialize(measures_results, order:)
8
+ self.measures_results = measures_results
9
+ self.measures = measures_results.keys
10
+ self.order = order
11
+ end
12
+
13
+ def perform
14
+ {
15
+ entities: entities,
16
+ datasets: datasets
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ def entities
23
+ sorted_dimension_keys.map do |key|
24
+ Utils.dimension_key_to_entity(key, primary_dimension_with_measure, dimension_ids_dimension_instances)
25
+ end
26
+ end
27
+
28
+ def datasets
29
+ sorted_measures_results.map do |measure, result|
30
+ values = result.values.map do |raw_value|
31
+ Utils.raw_value_to_value(raw_value, measure.value_format_method)
32
+ end
33
+ {
34
+ entity: measure,
35
+ values: values
36
+ }
37
+ end
38
+ end
39
+
40
+ def dimension_summaries
41
+ @dimension_summaries ||= dimension_keys.map do |dimension_key|
42
+ label = Utils.dimension_key_to_label(dimension_key, primary_dimension_with_measure, dimension_ids_dimension_instances)
43
+ [dimension_key, label]
44
+ end
45
+ end
46
+
47
+ def sorted_dimension_keys
48
+ sorted_measures_results.first.last.keys
49
+ end
50
+
51
+ def dimension_keys_sorted_by_label
52
+ @dimension_keys_sorted_by_label ||= dimension_summaries.sort_by(&:last).map(&:first)
53
+ end
54
+
55
+ def dimension_keys
56
+ measures_results.first.last.keys
57
+ end
58
+
59
+ def dimension_ids_dimension_instances
60
+ @dimension_ids_dimension_instances ||= begin
61
+ dimension_ids = dimension_keys
62
+ Utils.dimension_to_dimension_ids_dimension_instances(primary_dimension_with_measure, dimension_ids)
63
+ end
64
+ end
65
+
66
+ def primary_dimension_with_measure
67
+ @primary_dimension_with_measure ||= DimensionWithMeasure.new(dimension: primary_measure.dimensions.first, measure: primary_measure)
68
+ end
69
+
70
+ def primary_measure
71
+ measures.first
72
+ end
73
+
74
+ def sorted_measures_results
75
+ @sorted_measures_results ||= begin
76
+ if order.relation == 'dimension1' && order.field == 'label'
77
+ sorted_measures_results = measures_results.map do |measure, dimension_keys_values|
78
+ sorted_dimension_keys_values = dimension_keys_values.sort_by { |key, _| dimension_keys_sorted_by_label.index(key) }
79
+ sorted_dimension_keys_values = sorted_dimension_keys_values.reverse if order.direction == 'desc'
80
+ [measure, Hash[sorted_dimension_keys_values]]
81
+ end
82
+ elsif (order.relation == 'dimension1' && order.field.nil?) || (order.relation == 0)
83
+ sorted_measures_results = measures_results.map do |measure, dimension_keys_values|
84
+ sorted_dimension_keys_values = dimension_keys_values.sort_by(&:first)
85
+ sorted_dimension_keys_values = sorted_dimension_keys_values.reverse if order.direction == 'desc'
86
+ [measure, Hash[sorted_dimension_keys_values]]
87
+ end
88
+ elsif order.relation.is_a?(Fixnum)
89
+ measure_index = order.relation - 1
90
+ raise ArgumentError.new("Invalid order column: #{order.relation}") unless measure_index.in?((0..(measures_results.length - 1)))
91
+ dimension_keys_values = measures_results.values.to_a[measure_index]
92
+ sorted_dimension_keys = dimension_keys_values.sort_by(&:last).map(&:first)
93
+ sorted_dimension_keys = sorted_dimension_keys.reverse if order.direction == 'desc'
94
+ sorted_measures_results = measures_results.map do |measure, dimension_keys_values|
95
+ dimension_keys_values = dimension_keys_values.sort_by { |dimension_key, _| sorted_dimension_keys.index(dimension_key) }
96
+ [measure, Hash[dimension_keys_values]]
97
+ end
98
+ elsif order.relation == 'count'
99
+ dimension_keys_sums = Hash.new(0)
100
+ measures_results.values.each do |dimension_keys_values|
101
+ dimension_keys_values.each do |dimension_key, value|
102
+ dimension_keys_sums[dimension_key] += value
103
+ end
104
+ end
105
+ sorted_dimension_keys = dimension_keys_sums.sort_by(&:last).map(&:first)
106
+ sorted_dimension_keys = sorted_dimension_keys.reverse if order.direction == 'desc'
107
+ sorted_measures_results = measures_results.map do |measure, dimension_keys_values|
108
+ dimension_keys_values = dimension_keys_values.sort_by { |dimension_key, _| sorted_dimension_keys.index(dimension_key) }
109
+ [measure, Hash[dimension_keys_values]]
110
+ end
111
+ else
112
+ sorted_measures_results = measures_results
113
+ end
114
+ Hash[sorted_measures_results]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,141 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class FormatTwoDimensions
5
+ attr_accessor :measure, :dimension, :second_dimension, :dimension_keys_values, :order
6
+
7
+ def initialize(measure, dimension_keys_values, order:)
8
+ self.measure = measure
9
+ self.dimension = measure.dimensions[0]
10
+ self.second_dimension = measure.dimensions[1]
11
+ self.dimension_keys_values = dimension_keys_values
12
+ self.order = order
13
+ end
14
+
15
+ def perform
16
+ {
17
+ entities: entities,
18
+ datasets: datasets
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def entities
25
+ sorted_primary_keys.map do |primary_key|
26
+ Utils.dimension_key_to_entity(primary_key, dimension, dimension_ids_dimension_instances)
27
+ end
28
+ end
29
+
30
+ def datasets
31
+ secondary_keys_values = sorted_secondary_keys.map do |secondary_key|
32
+ raw_values = sorted_primary_keys.map do |primary_key|
33
+ primary_keys_secondary_keys_values[primary_key][secondary_key]
34
+ end
35
+ values = raw_values.map do |raw_value|
36
+ Utils.raw_value_to_value(raw_value, measure.value_format_method)
37
+ end
38
+ [secondary_key, values]
39
+ end
40
+ secondary_keys_values.map do |secondary_key, values|
41
+ next if secondary_key.blank?
42
+ {
43
+ entity: Utils.dimension_key_to_entity(secondary_key, second_dimension, second_dimension_ids_dimension_instances),
44
+ values: values
45
+ }
46
+ end.compact
47
+ end
48
+
49
+ def sorted_primary_keys_secondary_keys_values
50
+ @sorted_primary_keys_secondary_keys_values ||= begin
51
+ if order.relation == 'dimension1' && order.field == 'label'
52
+ primary_keys_secondary_keys_values
53
+ sorted_primary_keys_secondary_keys_values = primary_keys_secondary_keys_values.sort_by do |primary_key, _|
54
+ primary_dimension_keys_sorted_by_label.index(primary_key)
55
+ end
56
+ elsif order.relation == 'dimension1' && order.field.nil?
57
+ sorted_primary_keys_secondary_keys_values = primary_keys_secondary_keys_values.sort_by do |primary_key, _|
58
+ primary_key
59
+ end
60
+ elsif order.relation == 'count'
61
+ primary_keys_sums = Hash.new(0)
62
+ primary_keys_secondary_keys_values.each do |primary_key, secondary_keys_values|
63
+ primary_keys_sums[primary_key] += secondary_keys_values.values.sum
64
+ end
65
+ sorted_primary_keys = primary_keys_sums.sort_by(&:last).map(&:first)
66
+ sorted_primary_keys_secondary_keys_values = primary_keys_secondary_keys_values.sort_by do |primary_key, _|
67
+ sorted_primary_keys.index(primary_key)
68
+ end
69
+ else
70
+ dimension_keys_values
71
+ end
72
+ sorted_primary_keys_secondary_keys_values = sorted_primary_keys_secondary_keys_values.reverse if order.direction == 'desc'
73
+ Hash[sorted_primary_keys_secondary_keys_values]
74
+ end
75
+ end
76
+
77
+ def primary_keys_secondary_keys_values
78
+ @primary_keys_secondary_keys_values ||= begin
79
+ primary_keys_secondary_keys_values = {}
80
+ dimension_keys_values.each do |(primary_key, secondary_key), value|
81
+ primary_key = primary_key.to_date if primary_key.is_a?(Time)
82
+ secondary_key = secondary_key.to_date if secondary_key.is_a?(Time)
83
+ primary_keys_secondary_keys_values[primary_key] ||= {}
84
+ primary_keys_secondary_keys_values[primary_key][secondary_key] = value
85
+ end
86
+ primary_keys_secondary_keys_values
87
+ end
88
+ end
89
+
90
+ def dimension_ids_dimension_instances
91
+ @dimension_ids_dimension_instances ||= begin
92
+ Utils.dimension_to_dimension_ids_dimension_instances(dimension, primary_keys)
93
+ end
94
+ end
95
+
96
+ def second_dimension_ids_dimension_instances
97
+ @second_dimension_ids_dimension_instances ||= begin
98
+ Utils.dimension_to_dimension_ids_dimension_instances(second_dimension, secondary_keys)
99
+ end
100
+ end
101
+
102
+ def sorted_primary_keys
103
+ @sorted_primary_keys ||= begin
104
+ keys = sorted_primary_keys_secondary_keys_values.keys
105
+ limit = dimension.dimension_instances_limit
106
+ keys = keys.first(limit) if limit
107
+ keys
108
+ end
109
+ end
110
+
111
+ def sorted_secondary_keys
112
+ @sorted_secondary_keys ||= begin
113
+ keys = sorted_primary_keys_secondary_keys_values.values.first.keys
114
+ limit = second_dimension.dimension_instances_limit
115
+ keys = keys.first(limit) if limit
116
+ keys
117
+ end
118
+ end
119
+
120
+ def primary_summaries
121
+ primary_keys.map do |key|
122
+ label = Utils.dimension_key_to_label(key, dimension, dimension_ids_dimension_instances)
123
+ [key, label]
124
+ end
125
+ end
126
+
127
+ def primary_dimension_keys_sorted_by_label
128
+ @primary_dimension_keys_sorted_by_label ||= primary_summaries.sort_by(&:last).map(&:first)
129
+ end
130
+
131
+ def primary_keys
132
+ @primary_keys ||= dimension_keys_values.keys.map(&:first).uniq
133
+ end
134
+
135
+ def secondary_keys
136
+ @secondary_keys ||= dimension_keys_values.keys.map(&:last).uniq
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end