reports_kit 0.0.4 → 0.1.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.
@@ -0,0 +1,15 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Adapters
4
+ class Postgresql
5
+ def self.truncate_to_day(column)
6
+ "#{column}::date"
7
+ end
8
+
9
+ def self.truncate_to_week(column)
10
+ "DATE_TRUNC('week', #{column}::timestamp)"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -44,6 +44,7 @@ module ReportsKit
44
44
  usePointStyle: true
45
45
  }
46
46
  },
47
+ maintainAspectRatio: false,
47
48
  tooltips: {
48
49
  xPadding: 8,
49
50
  yPadding: 7
@@ -73,6 +74,22 @@ module ReportsKit
73
74
  private
74
75
 
75
76
  def set_colors
77
+ if donut_or_pie_chart?
78
+ set_record_scoped_colors
79
+ else
80
+ set_dataset_scoped_colors
81
+ end
82
+ end
83
+
84
+ def set_record_scoped_colors
85
+ self.data[:chart_data][:datasets] = self.data[:chart_data][:datasets].map do |dataset|
86
+ length = dataset[:data].length
87
+ dataset[:backgroundColor] = DEFAULT_COLORS * (length.to_f / DEFAULT_COLORS.length).ceil
88
+ dataset
89
+ end
90
+ end
91
+
92
+ def set_dataset_scoped_colors
76
93
  self.data[:chart_data][:datasets] = data[:chart_data][:datasets].map.with_index do |dataset, index|
77
94
  color = DEFAULT_COLORS[index % DEFAULT_COLORS.length]
78
95
  dataset[:backgroundColor] = color
@@ -83,6 +100,8 @@ module ReportsKit
83
100
 
84
101
  def default_options
85
102
  @default_options ||= begin
103
+ return {} if donut_or_pie_chart?
104
+
86
105
  default_options = DEFAULT_OPTIONS.deep_dup
87
106
 
88
107
  x_axis_label = options[:x_axis_label]
@@ -126,6 +145,10 @@ module ReportsKit
126
145
  return if type.blank?
127
146
  self.data[:type] = type
128
147
  end
148
+
149
+ def donut_or_pie_chart?
150
+ type.in?(%w(donut pie))
151
+ end
129
152
  end
130
153
  end
131
154
  end
@@ -32,7 +32,7 @@ module ReportsKit
32
32
  relation = relation.order('2')
33
33
  end
34
34
  dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
35
- dimension_keys_values = Utils.populate_sparse_values(dimension_keys_values)
35
+ dimension_keys_values = Utils.populate_sparse_hash(dimension_keys_values, dimension: dimension)
36
36
  dimension_keys_values.delete(nil)
37
37
  dimension_keys_values.delete('')
38
38
  dimension_keys_values
@@ -35,11 +35,12 @@ module ReportsKit
35
35
  else
36
36
  relation = relation.order('2')
37
37
  end
38
- dimension_keys_values = relation.count
38
+ dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
39
39
 
40
40
  if dimension.should_be_sorted_by_count?
41
41
  dimension_keys_values = sort_dimension_keys_values_by_count(dimension_keys_values)
42
42
  end
43
+ dimension_keys_values = Utils.populate_sparse_hash(dimension_keys_values, dimension: dimension)
43
44
  Hash[dimension_keys_values]
44
45
  end
45
46
  end
@@ -48,6 +49,8 @@ module ReportsKit
48
49
  @primary_keys_secondary_keys_values ||= begin
49
50
  primary_keys_secondary_keys_values = {}
50
51
  dimension_keys_values.each do |(primary_key, secondary_key), value|
52
+ primary_key = primary_key.to_date if primary_key.is_a?(Time)
53
+ secondary_key = secondary_key.to_date if secondary_key.is_a?(Time)
51
54
  primary_keys_secondary_keys_values[primary_key] ||= {}
52
55
  primary_keys_secondary_keys_values[primary_key][secondary_key] = value
53
56
  end
@@ -91,16 +94,17 @@ module ReportsKit
91
94
  end
92
95
  secondary_keys_values = secondary_keys_values.sort_by { |_, values| values.sum }.reverse
93
96
  secondary_keys_values.map do |secondary_key, values|
97
+ next if secondary_key.blank?
94
98
  {
95
99
  label: Utils.dimension_key_to_label(secondary_key, second_dimension, second_dimension_ids_dimension_instances),
96
100
  data: values
97
101
  }
98
- end
102
+ end.compact
99
103
  end
100
104
 
101
105
  def primary_keys
102
106
  @primary_keys ||= begin
103
- keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:first).uniq)
107
+ keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:first).uniq, dimension: dimension)
104
108
  if dimension.should_be_sorted_by_count?
105
109
  limit = dimension.dimension_instances_limit
106
110
  keys = keys.first(limit) if limit
@@ -111,7 +115,7 @@ module ReportsKit
111
115
 
112
116
  def secondary_keys
113
117
  @secondary_keys ||= begin
114
- keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:last).uniq)
118
+ keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:last).uniq, dimension: second_dimension)
115
119
  limit = second_dimension.dimension_instances_limit
116
120
  keys = keys.first(limit) if limit
117
121
  keys
@@ -6,50 +6,85 @@ module ReportsKit
6
6
  time.strftime('%b %-d, \'%y')
7
7
  end
8
8
 
9
- def self.populate_sparse_values(dimension_keys_values, use_first_value_key: false)
10
- return dimension_keys_values if dimension_keys_values.blank?
11
- first_key = dimension_keys_values.first.first
12
- return dimension_keys_values unless first_key.is_a?(Time)
13
-
14
- beginning_of_current_week = Time.now.utc.beginning_of_week(ReportsKit.configuration.first_day_of_week)
15
- last_key = dimension_keys_values.to_a.last.first
16
- last_key = [beginning_of_current_week, last_key].compact.max
17
-
18
- time = first_key
19
- full_dimension_instances_values = []
20
- if use_first_value_key
21
- first_value_key = dimension_keys_values.first.last.keys.first
22
- blank_value = { first_value_key => 0 }
9
+ def self.populate_sparse_hash(hash, dimension:)
10
+ return hash if hash.blank?
11
+ keys = hash.keys
12
+ is_nested = false
13
+ if keys.first.is_a?(Array)
14
+ is_nested = true
15
+ keys_values = arrays_values_to_nested_hash(hash)
16
+ keys = keys_values.keys
23
17
  else
24
- blank_value = 0
18
+ keys_values = hash
25
19
  end
26
- loop do
27
- full_dimension_instances_values << [time, dimension_keys_values[time] || blank_value]
28
- break if time >= last_key
29
- time += 1.week
20
+
21
+ first_key = dimension.first_key || keys.first
22
+ return hash unless first_key.is_a?(Time) || first_key.is_a?(Date)
23
+ keys_values = keys_values.map do |key, value|
24
+ key = key.to_date if key.is_a?(Time)
25
+ [key, value]
26
+ end.to_h
27
+
28
+ keys = populate_sparse_keys(keys, dimension: dimension)
29
+ populated_keys_values = {}
30
+ default_value = is_nested ? {} : 0
31
+ keys.each do |key|
32
+ populated_keys_values[key] = keys_values[key] || default_value
30
33
  end
31
- Hash[full_dimension_instances_values]
34
+ return nested_hash_to_arrays_values(populated_keys_values) if is_nested
35
+ populated_keys_values
32
36
  end
33
37
 
34
- def self.populate_sparse_keys(keys)
38
+ def self.populate_sparse_keys(keys, dimension:)
35
39
  return keys if keys.blank?
36
- first_key = keys.first
37
- return keys unless first_key.is_a?(Time)
40
+ first_key = dimension.first_key || keys.first
41
+ return keys unless first_key.is_a?(Time) || first_key.is_a?(Date)
42
+ granularity = dimension.granularity
43
+
44
+ first_key = first_key.beginning_of_week if granularity == 'week'
38
45
  keys = keys.sort
39
- beginning_of_current_week = Time.now.utc.beginning_of_week(ReportsKit.configuration.first_day_of_week)
40
- last_key = keys.last
41
- last_key = [beginning_of_current_week, last_key].compact.max
46
+ last_key = dimension.last_key || keys.last
47
+ last_key = last_key.beginning_of_week if granularity == 'week'
48
+
49
+ if granularity == 'week'
50
+ beginning_of_current_week = Time.now.utc.beginning_of_week(ReportsKit.configuration.first_day_of_week)
51
+ last_key = [beginning_of_current_week, last_key].compact.max
52
+ end
42
53
 
43
- time = first_key
54
+ date = first_key.to_date
44
55
  populated_keys = []
56
+ interval = granularity == 'week' ? 1.week : 1.day
45
57
  loop do
46
- populated_keys << time
47
- break if time >= last_key
48
- time += 1.week
58
+ populated_keys << date
59
+ break if date >= last_key
60
+ date += interval
49
61
  end
50
62
  populated_keys
51
63
  end
52
64
 
65
+ def self.arrays_values_to_nested_hash(arrays_values)
66
+ nested_hash = {}
67
+ arrays_values.each do |(key1, key2), value|
68
+ nested_hash[key1] ||= {}
69
+ nested_hash[key1][key2] ||= value
70
+ end
71
+ nested_hash
72
+ end
73
+
74
+ def self.nested_hash_to_arrays_values(nested_hash)
75
+ arrays_values = {}
76
+ nested_hash.each do |key1, key2s_values|
77
+ if key2s_values.blank?
78
+ arrays_values[[key1, nil]] = 0
79
+ next
80
+ end
81
+ key2s_values.each do |key2, value|
82
+ arrays_values[[key1, key2]] = value
83
+ end
84
+ end
85
+ arrays_values
86
+ end
87
+
53
88
  def self.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
54
89
  return nil unless dimension.instance_class
55
90
  dimension_instances = dimension.instance_class.where(id: dimension_ids.uniq)
@@ -62,12 +97,12 @@ module ReportsKit
62
97
  def self.dimension_key_to_label(dimension_instance, dimension, ids_dimension_instances)
63
98
  return dimension_instance.to_s if dimension.configured_by_column? && dimension.column_type == :integer
64
99
  case dimension_instance
65
- when Time
100
+ when Time, Date
66
101
  Utils.format_time(dimension_instance)
67
102
  when Fixnum
68
103
  raise ArgumentError.new("ids_dimension_instances must be present for Dimension with identifier: #{dimension_instance}") unless ids_dimension_instances
69
104
  instance = ids_dimension_instances[dimension_instance.to_i]
70
- raise ArgumentError.new("instance could not be found for Dimension with identifier: #{dimension_instance}") unless instance
105
+ return unless instance
71
106
  instance.to_s
72
107
  else
73
108
  dimension_instance.to_s.gsub(/\.0$/, '')
@@ -2,6 +2,12 @@ module ReportsKit
2
2
  module Reports
3
3
  class Dimension
4
4
  DEFAULT_DIMENSION_INSTANCES_LIMIT = 30
5
+ DEFAULT_GRANULARITY = 'week'
6
+ VALID_GRANULARITIES = %w(day week).freeze
7
+ ADAPTER_NAMES_CLASSES = {
8
+ 'mysql2' => Adapters::Mysql,
9
+ 'postgresql' => Adapters::Postgresql
10
+ }.freeze
5
11
 
6
12
  attr_accessor :properties, :measure, :configuration
7
13
 
@@ -26,6 +32,15 @@ module ReportsKit
26
32
  properties[:key]
27
33
  end
28
34
 
35
+ def granularity
36
+ @granularity ||= begin
37
+ return unless configured_by_time?
38
+ granularity = properties[:granularity] || DEFAULT_GRANULARITY
39
+ raise ArgumentError.new("Invalid granularity: #{granularity}") unless VALID_GRANULARITIES.include?(granularity)
40
+ granularity
41
+ end
42
+ end
43
+
29
44
  def label
30
45
  key.titleize
31
46
  end
@@ -50,9 +65,9 @@ module ReportsKit
50
65
  elsif configured_by_association?
51
66
  "#{model_class.table_name}.#{reflection.foreign_key}"
52
67
  elsif configured_by_column? && configured_by_time?
53
- "date_trunc('week', #{model_class.table_name}.#{key}::timestamp)"
68
+ granularity == 'day' ? day_expression : week_expression
54
69
  elsif configured_by_column?
55
- "#{model_class.table_name}.#{key}"
70
+ column_expression
56
71
  else
57
72
  raise ArgumentError.new('Invalid group_expression')
58
73
  end
@@ -70,9 +85,45 @@ module ReportsKit
70
85
  end
71
86
  end
72
87
 
88
+ def first_key
89
+ return unless configured_by_time? && datetime_filters.present?
90
+ datetime_filters.map(&:start_at).compact.sort.first
91
+ end
92
+
93
+ def last_key
94
+ return unless configured_by_time? && datetime_filters.present?
95
+ datetime_filters.map(&:end_at).compact.sort.last
96
+ end
97
+
98
+ def datetime_filters
99
+ return [] unless measure.filters.present?
100
+ measure.filters.map(&:filter_type).select { |filter_type| filter_type.is_a?(FilterTypes::Datetime) }
101
+ end
102
+
73
103
  def should_be_sorted_by_count?
74
104
  !configured_by_time?
75
105
  end
106
+
107
+ def adapter
108
+ @adapter ||= begin
109
+ adapter_name = model_class.connection_config[:adapter]
110
+ adapter = ADAPTER_NAMES_CLASSES[adapter_name]
111
+ raise ArgumentError.new("Unsupported adapter: #{adapter_name}") unless adapter
112
+ adapter
113
+ end
114
+ end
115
+
116
+ def column_expression
117
+ "#{model_class.table_name}.#{key}"
118
+ end
119
+
120
+ def day_expression
121
+ adapter.truncate_to_day(column_expression)
122
+ end
123
+
124
+ def week_expression
125
+ adapter.truncate_to_week(column_expression)
126
+ end
76
127
  end
77
128
  end
78
129
  end
@@ -7,21 +7,36 @@ module ReportsKit
7
7
  }
8
8
 
9
9
  def apply_conditions(records)
10
+ case conditions
11
+ when ::String
12
+ records.where("(#{conditions}) #{sql_operator} true")
13
+ when ::Hash
14
+ boolean_operator ? records.where(conditions) : records.not.where(conditions)
15
+ else
16
+ raise ArgumentError.new("Unsupported conditions type: '#{conditions}'")
17
+ end
18
+ end
19
+
20
+ def boolean_operator
10
21
  case criteria[:operator]
11
22
  when true, 'true'
12
- records.where("(#{column}) = true")
23
+ true
13
24
  when false, 'false'
14
- records.where("(#{column}) != true")
25
+ false
15
26
  else
16
27
  raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
17
28
  end
18
29
  end
19
30
 
31
+ def sql_operator
32
+ boolean_operator ? '=' : '!='
33
+ end
34
+
20
35
  def valid?
21
36
  criteria[:operator].present?
22
37
  end
23
38
 
24
- def column
39
+ def conditions
25
40
  settings[:conditions] || properties[:key]
26
41
  end
27
42
  end
@@ -9,15 +9,29 @@ module ReportsKit
9
9
  def apply_conditions(records)
10
10
  case criteria[:operator]
11
11
  when 'between'
12
- start_string, end_string = value.split(' - ')
13
- start_at = Date.parse(start_string)
14
- end_at = Date.parse(end_string)
15
12
  records.where("#{column} IS NOT NULL").where("#{column} BETWEEN ? AND ?", start_at, end_at)
16
13
  else
17
14
  raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
18
15
  end
19
16
  end
20
17
 
18
+ def start_at_end_at
19
+ @start_at_end_at ||= begin
20
+ start_string, end_string = value.split(' - ')
21
+ start_at = Date.parse(start_string)
22
+ end_at = Date.parse(end_string)
23
+ [start_at, end_at]
24
+ end
25
+ end
26
+
27
+ def start_at
28
+ start_at_end_at[0]
29
+ end
30
+
31
+ def end_at
32
+ start_at_end_at[1]
33
+ end
34
+
21
35
  def valid?
22
36
  value.present?
23
37
  end
@@ -12,17 +12,25 @@ module ReportsKit
12
12
 
13
13
  def perform
14
14
  raise ArgumentError.new("Could not find a model for filter_key: '#{filter_key}'") unless model
15
- results = model
15
+ results = relation
16
16
  results = results.public_send(scope) if scope
17
17
  results = results.limit(10_000)
18
18
  results = results.map { |result| { id: result.id, text: result.to_s } }
19
19
  results = results.sort_by { |result| result[:text].downcase }
20
20
  results = filter_results(results)
21
- results
21
+ results.first(100)
22
22
  end
23
23
 
24
24
  private
25
25
 
26
+ def relation
27
+ if context_record
28
+ context_record.public_send(model.name.tableize)
29
+ else
30
+ model
31
+ end
32
+ end
33
+
26
34
  def filter_results(results)
27
35
  query = params[:q].try(:downcase)
28
36
  if query.present?
@@ -5,7 +5,8 @@ module ReportsKit
5
5
  :boolean,
6
6
  :datetime,
7
7
  :integer,
8
- :string
8
+ :string,
9
+ :text
9
10
  ]
10
11
 
11
12
  attr_accessor :inferrable, :inferrable_type