reports_kit 0.0.1

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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rubocop.yml +83 -0
  4. data/Gemfile +3 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +468 -0
  7. data/Rakefile +2 -0
  8. data/app/assets/javascripts/reports_kit/application.js +14 -0
  9. data/app/assets/javascripts/reports_kit/lib/_init.js +8 -0
  10. data/app/assets/javascripts/reports_kit/lib/chart.js +39 -0
  11. data/app/assets/javascripts/reports_kit/lib/report.js +119 -0
  12. data/app/assets/javascripts/reports_kit/vendor/chart.js +12269 -0
  13. data/app/assets/javascripts/reports_kit/vendor/daterangepicker.js +1627 -0
  14. data/app/assets/javascripts/reports_kit/vendor/moment.js +4040 -0
  15. data/app/assets/javascripts/reports_kit/vendor/select2.full.js +6436 -0
  16. data/app/assets/stylesheets/reports_kit/application.css.scss +3 -0
  17. data/app/assets/stylesheets/reports_kit/reports.css.sass +7 -0
  18. data/app/assets/stylesheets/reports_kit/select2_overrides.css.sass +7 -0
  19. data/app/assets/stylesheets/reports_kit/vendor/daterangepicker.css +269 -0
  20. data/app/assets/stylesheets/reports_kit/vendor/select2-bootstrap.css +721 -0
  21. data/app/assets/stylesheets/reports_kit/vendor/select2.css +484 -0
  22. data/config/routes.rb +10 -0
  23. data/docs/images/chart_options.png +0 -0
  24. data/docs/images/dashed_line.png +0 -0
  25. data/docs/images/flights_by_carrier.png +0 -0
  26. data/docs/images/flights_by_carrier_and_flight_at.png +0 -0
  27. data/docs/images/flights_by_delay.png +0 -0
  28. data/docs/images/flights_by_flight_at.png +0 -0
  29. data/docs/images/flights_by_hours_delayed.png +0 -0
  30. data/docs/images/flights_with_check_box.png +0 -0
  31. data/docs/images/flights_with_configured_boolean.png +0 -0
  32. data/docs/images/flights_with_configured_datetime.png +0 -0
  33. data/docs/images/flights_with_configured_number.png +0 -0
  34. data/docs/images/flights_with_configured_string.png +0 -0
  35. data/docs/images/flights_with_date_range.png +0 -0
  36. data/docs/images/flights_with_filters.png +0 -0
  37. data/docs/images/flights_with_multi_autocomplete.png +0 -0
  38. data/docs/images/flights_with_string_filter.png +0 -0
  39. data/docs/images/horizontal_bar.png +0 -0
  40. data/docs/images/legend_right.png +0 -0
  41. data/docs/images/users_by_created_at.png +0 -0
  42. data/gists/doc.txt +58 -0
  43. data/lib/reports_kit.rb +17 -0
  44. data/lib/reports_kit/base_controller.rb +11 -0
  45. data/lib/reports_kit/configuration.rb +10 -0
  46. data/lib/reports_kit/engine.rb +21 -0
  47. data/lib/reports_kit/helper.rb +19 -0
  48. data/lib/reports_kit/model.rb +16 -0
  49. data/lib/reports_kit/model_configuration.rb +23 -0
  50. data/lib/reports_kit/rails.rb +5 -0
  51. data/lib/reports_kit/report_builder.rb +76 -0
  52. data/lib/reports_kit/reports/data/chart_options.rb +132 -0
  53. data/lib/reports_kit/reports/data/generate.rb +65 -0
  54. data/lib/reports_kit/reports/data/one_dimension.rb +71 -0
  55. data/lib/reports_kit/reports/data/two_dimensions.rb +129 -0
  56. data/lib/reports_kit/reports/data/utils.rb +79 -0
  57. data/lib/reports_kit/reports/dimension.rb +78 -0
  58. data/lib/reports_kit/reports/filter.rb +84 -0
  59. data/lib/reports_kit/reports/filter_types/base.rb +47 -0
  60. data/lib/reports_kit/reports/filter_types/boolean.rb +30 -0
  61. data/lib/reports_kit/reports/filter_types/datetime.rb +27 -0
  62. data/lib/reports_kit/reports/filter_types/number.rb +28 -0
  63. data/lib/reports_kit/reports/filter_types/records.rb +26 -0
  64. data/lib/reports_kit/reports/filter_types/string.rb +38 -0
  65. data/lib/reports_kit/reports/generate_autocomplete_results.rb +55 -0
  66. data/lib/reports_kit/reports/inferrable_configuration.rb +113 -0
  67. data/lib/reports_kit/reports/measure.rb +58 -0
  68. data/lib/reports_kit/reports_controller.rb +15 -0
  69. data/lib/reports_kit/resources_controller.rb +8 -0
  70. data/lib/reports_kit/version.rb +3 -0
  71. data/reports_kit.gemspec +23 -0
  72. data/spec/factories/issue_factory.rb +4 -0
  73. data/spec/factories/issues_label_factory.rb +4 -0
  74. data/spec/factories/label_factory.rb +4 -0
  75. data/spec/factories/repo_factory.rb +5 -0
  76. data/spec/factories/tag_factory.rb +4 -0
  77. data/spec/fixtures/generate_inputs.yml +35 -0
  78. data/spec/fixtures/generate_outputs.yml +208 -0
  79. data/spec/reports_kit/reports/data/generate_spec.rb +275 -0
  80. data/spec/reports_kit/reports/dimension_spec.rb +38 -0
  81. data/spec/reports_kit/reports/filter_spec.rb +38 -0
  82. data/spec/spec_helper.rb +58 -0
  83. data/spec/support/factory_girl.rb +5 -0
  84. data/spec/support/helpers.rb +13 -0
  85. data/spec/support/models/issue.rb +6 -0
  86. data/spec/support/models/issues_label.rb +4 -0
  87. data/spec/support/models/label.rb +5 -0
  88. data/spec/support/models/repo.rb +7 -0
  89. data/spec/support/models/tag.rb +4 -0
  90. data/spec/support/schema.rb +38 -0
  91. metadata +232 -0
@@ -0,0 +1,65 @@
1
+ module ReportsKit
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)
10
+ self.properties = properties.deep_symbolize_keys
11
+ self.context_record = context_record
12
+ end
13
+
14
+ def perform
15
+ if second_dimension
16
+ data = Data::TwoDimensions.new(measure, dimension, second_dimension).perform
17
+ else
18
+ data = Data::OneDimension.new(measure, dimension).perform
19
+ end
20
+
21
+ ChartOptions.new(data, options: properties[:chart], inferred_options: inferred_options).perform
22
+ end
23
+
24
+ private
25
+
26
+ def measure
27
+ @measure ||= begin
28
+ measure_hash = properties[:measure]
29
+ raise ArgumentError.new('The number of measures must be exactly one') if measure_hash.blank?
30
+ Measure.new(measure_hash, context_record: context_record)
31
+ end
32
+ end
33
+
34
+ def dimension
35
+ @dimension ||= begin
36
+ Dimension.new(dimension_hashes[0], measure: measure)
37
+ end
38
+ end
39
+
40
+ def second_dimension
41
+ @second_dimension ||= begin
42
+ Dimension.new(dimension_hashes[1], measure: measure) if dimension_hashes[1]
43
+ end
44
+ end
45
+
46
+ def dimension_hashes
47
+ @dimension_hashes ||= begin
48
+ dimension_hashes = properties[:dimensions]
49
+ raise ArgumentError.new('Blank dimensions') if dimension_hashes.blank?
50
+ raise ArgumentError.new('The number of dimensions must be 1-2') unless dimension_hashes.length.in?([1, 2])
51
+ dimension_hashes = dimension_hashes.values if dimension_hashes.is_a?(Hash) && dimension_hashes.key?(:'0')
52
+ dimension_hashes
53
+ end
54
+ end
55
+
56
+ def inferred_options
57
+ {
58
+ x_axis_label: dimension.label,
59
+ y_axis_label: measure.label
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,71 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class OneDimension
5
+ attr_accessor :measure, :dimension
6
+
7
+ def initialize(measure, dimension)
8
+ self.measure = measure
9
+ self.dimension = dimension
10
+ end
11
+
12
+ def perform
13
+ {
14
+ chart_data: {
15
+ labels: labels,
16
+ datasets: datasets
17
+ }
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def dimension_keys_values
24
+ @dimension_keys_values ||= begin
25
+ relation = measure.filtered_relation
26
+ relation = relation.group(dimension.group_expression)
27
+ relation = relation.joins(dimension.joins) if dimension.joins
28
+ relation = relation.limit(dimension.dimension_instances_limit) if dimension.dimension_instances_limit
29
+ if dimension.should_be_sorted_by_count?
30
+ relation = relation.order('1 DESC')
31
+ else
32
+ relation = relation.order('2')
33
+ end
34
+ dimension_keys_values = relation.distinct.public_send(*measure.aggregate_function)
35
+ dimension_keys_values = Utils.populate_sparse_values(dimension_keys_values)
36
+ dimension_keys_values.delete(nil)
37
+ dimension_keys_values.delete('')
38
+ dimension_keys_values
39
+ end
40
+ end
41
+
42
+ def datasets
43
+ [
44
+ {
45
+ label: measure.label,
46
+ data: values
47
+ }
48
+ ]
49
+ end
50
+
51
+ def values
52
+ dimension_keys_values.values.map { |value| value.round(Generate::ROUND_PRECISION) }
53
+ end
54
+
55
+ def labels
56
+ keys = dimension_keys_values.keys
57
+ keys.map do |key|
58
+ Utils.dimension_key_to_label(key, dimension, dimension_ids_dimension_instances)
59
+ end
60
+ end
61
+
62
+ def dimension_ids_dimension_instances
63
+ @dimension_ids_dimension_instances ||= begin
64
+ dimension_ids = dimension_keys_values.keys
65
+ Utils.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,129 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class TwoDimensions
5
+ attr_accessor :measure, :dimension, :second_dimension
6
+
7
+ def initialize(measure, dimension, second_dimension)
8
+ self.measure = measure
9
+ self.dimension = dimension
10
+ self.second_dimension = second_dimension
11
+ end
12
+
13
+ def perform
14
+ {
15
+ chart_data: {
16
+ labels: labels,
17
+ datasets: datasets
18
+ }
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def dimension_keys_values
25
+ @dimension_keys_values ||= begin
26
+ relation = measure.filtered_relation
27
+ relation = measure.conditions.call(relation) if measure.conditions
28
+ relation = relation.group(dimension.group_expression, second_dimension.group_expression)
29
+
30
+ relation = relation.joins(dimension.joins) if dimension.joins
31
+ relation = relation.joins(second_dimension.joins) if second_dimension.joins
32
+
33
+ if dimension.should_be_sorted_by_count?
34
+ relation = relation.order('1 DESC')
35
+ else
36
+ relation = relation.order('2')
37
+ end
38
+ dimension_keys_values = relation.count
39
+
40
+ if dimension.should_be_sorted_by_count?
41
+ dimension_keys_values = sort_dimension_keys_values_by_count(dimension_keys_values)
42
+ end
43
+ Hash[dimension_keys_values]
44
+ end
45
+ end
46
+
47
+ def primary_keys_secondary_keys_values
48
+ @primary_keys_secondary_keys_values ||= begin
49
+ primary_keys_secondary_keys_values = {}
50
+ dimension_keys_values.each do |(primary_key, secondary_key), value|
51
+ primary_keys_secondary_keys_values[primary_key] ||= {}
52
+ primary_keys_secondary_keys_values[primary_key][secondary_key] = value
53
+ end
54
+ primary_keys_secondary_keys_values
55
+ end
56
+ end
57
+
58
+ def sort_dimension_keys_values_by_count(dimension_keys_values)
59
+ primary_keys_counts = Hash.new(0)
60
+ dimension_keys_values.each do |(primary_key, secondary_key), count|
61
+ primary_keys_counts[primary_key] += count
62
+ end
63
+ primary_keys_counts = primary_keys_counts.to_a
64
+ sorted_primary_keys = primary_keys_counts.sort_by { |primary_key, count| count }.reverse.map(&:first)
65
+ dimension_keys_values = dimension_keys_values.sort_by { |(primary_key, secondary_key), count| sorted_primary_keys.index(primary_key) }
66
+ Hash[dimension_keys_values]
67
+ end
68
+
69
+ def dimension_ids
70
+ dimension_keys_values.keys.map(&:first)
71
+ end
72
+
73
+ def dimension_ids_dimension_instances
74
+ @dimension_ids_dimension_instances ||= begin
75
+ Utils.dimension_to_dimension_ids_dimension_instances(dimension, dimension_keys_values.keys.map(&:first))
76
+ end
77
+ end
78
+
79
+ def second_dimension_ids_dimension_instances
80
+ @second_dimension_ids_dimension_instances ||= begin
81
+ Utils.dimension_to_dimension_ids_dimension_instances(second_dimension, dimension_keys_values.keys.map(&:last))
82
+ end
83
+ end
84
+
85
+ def datasets
86
+ secondary_keys_values = secondary_keys.map do |secondary_key|
87
+ values = primary_keys.map do |primary_key|
88
+ primary_keys_secondary_keys_values[primary_key].try(:[], secondary_key) || 0
89
+ end
90
+ [secondary_key, values]
91
+ end
92
+ secondary_keys_values = secondary_keys_values.sort_by { |_, values| values.sum }.reverse
93
+ secondary_keys_values.map do |secondary_key, values|
94
+ {
95
+ label: Utils.dimension_key_to_label(secondary_key, second_dimension, second_dimension_ids_dimension_instances),
96
+ data: values
97
+ }
98
+ end
99
+ end
100
+
101
+ def primary_keys
102
+ @primary_keys ||= begin
103
+ keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:first).uniq)
104
+ if dimension.should_be_sorted_by_count?
105
+ limit = dimension.dimension_instances_limit
106
+ keys = keys.first(limit) if limit
107
+ end
108
+ keys
109
+ end
110
+ end
111
+
112
+ def secondary_keys
113
+ @secondary_keys ||= begin
114
+ keys = Utils.populate_sparse_keys(dimension_keys_values.keys.map(&:last).uniq)
115
+ limit = second_dimension.dimension_instances_limit
116
+ keys = keys.first(limit) if limit
117
+ keys
118
+ end
119
+ end
120
+
121
+ def labels
122
+ primary_keys.map do |primary_key|
123
+ Utils.dimension_key_to_label(primary_key, dimension, dimension_ids_dimension_instances)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,79 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module Data
4
+ class Utils
5
+ def self.format_time(time)
6
+ time.strftime('%b %-d, \'%y')
7
+ end
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 }
23
+ else
24
+ blank_value = 0
25
+ 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
30
+ end
31
+ Hash[full_dimension_instances_values]
32
+ end
33
+
34
+ def self.populate_sparse_keys(keys)
35
+ return keys if keys.blank?
36
+ first_key = keys.first
37
+ return keys unless first_key.is_a?(Time)
38
+ 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
42
+
43
+ time = first_key
44
+ populated_keys = []
45
+ loop do
46
+ populated_keys << time
47
+ break if time >= last_key
48
+ time += 1.week
49
+ end
50
+ populated_keys
51
+ end
52
+
53
+ def self.dimension_to_dimension_ids_dimension_instances(dimension, dimension_ids)
54
+ return nil unless dimension.instance_class
55
+ dimension_instances = dimension.instance_class.where(id: dimension_ids.uniq)
56
+ dimension_ids_dimension_instances = dimension_instances.map do |dimension_instance|
57
+ [dimension_instance.id, dimension_instance]
58
+ end
59
+ Hash[dimension_ids_dimension_instances]
60
+ end
61
+
62
+ def self.dimension_key_to_label(dimension_instance, dimension, ids_dimension_instances)
63
+ return dimension_instance.to_s if dimension.configured_by_column? && dimension.column_type == :integer
64
+ case dimension_instance
65
+ when Time
66
+ Utils.format_time(dimension_instance)
67
+ when Fixnum
68
+ raise ArgumentError.new("ids_dimension_instances must be present for Dimension with identifier: #{dimension_instance}") unless ids_dimension_instances
69
+ 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
71
+ instance.to_s
72
+ else
73
+ dimension_instance.to_s.gsub(/\.0$/, '')
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,78 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class Dimension
4
+ DEFAULT_DIMENSION_INSTANCES_LIMIT = 30
5
+
6
+ attr_accessor :properties, :measure, :configuration
7
+
8
+ delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
9
+ :settings_from_model, :reflection, :instance_class, :model_class, :column_type,
10
+ to: :configuration
11
+
12
+ def initialize(properties, measure:)
13
+ self.configuration = InferrableConfiguration.new(self, :dimensions)
14
+ self.measure = measure
15
+
16
+ raise ArgumentError.new('Blank properties') if properties.blank?
17
+ properties = { key: properties } if properties.is_a?(String)
18
+ raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
19
+ properties = properties.deep_symbolize_keys
20
+ self.properties = properties
21
+ missing_group_setting = settings && !settings.key?(:group)
22
+ raise ArgumentError.new("Dimension settings for dimension '#{key}' of #{model_class} must include :group") if missing_group_setting
23
+ end
24
+
25
+ def key
26
+ properties[:key]
27
+ end
28
+
29
+ def label
30
+ key.titleize
31
+ end
32
+
33
+ def settings
34
+ inferred_settings.merge(settings_from_model)
35
+ end
36
+
37
+ def inferred_settings
38
+ configuration.inferred_settings.merge(inferred_dimension_settings)
39
+ end
40
+
41
+ def inferred_dimension_settings
42
+ {
43
+ group: group_expression
44
+ }
45
+ end
46
+
47
+ def group_expression
48
+ if configured_by_model?
49
+ settings_from_model[:group]
50
+ elsif configured_by_association?
51
+ "#{model_class.table_name}.#{reflection.foreign_key}"
52
+ elsif configured_by_column? && configured_by_time?
53
+ "date_trunc('week', #{model_class.table_name}.#{key}::timestamp)"
54
+ elsif configured_by_column?
55
+ "#{model_class.table_name}.#{key}"
56
+ else
57
+ raise ArgumentError.new('Invalid group_expression')
58
+ end
59
+ end
60
+
61
+ def joins
62
+ settings_from_model[:joins] if configured_by_model?
63
+ end
64
+
65
+ def dimension_instances_limit
66
+ if configured_by_time?
67
+ properties[:limit]
68
+ else
69
+ properties[:limit] || DEFAULT_DIMENSION_INSTANCES_LIMIT
70
+ end
71
+ end
72
+
73
+ def should_be_sorted_by_count?
74
+ !configured_by_time?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,84 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class Filter
4
+ CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES = {
5
+ association: FilterTypes::Records,
6
+ boolean: FilterTypes::Boolean,
7
+ datetime: FilterTypes::Datetime,
8
+ integer: FilterTypes::Number,
9
+ string: FilterTypes::String
10
+ }
11
+ COLUMN_TYPES_FILTER_TYPE_CLASSES = {
12
+ boolean: FilterTypes::Boolean,
13
+ datetime: FilterTypes::Datetime,
14
+ integer: FilterTypes::Number,
15
+ string: FilterTypes::String
16
+ }
17
+
18
+ attr_accessor :properties, :measure, :configuration
19
+
20
+ delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
21
+ :settings_from_model, :configuration_strategy, :instance_class, :column_type, :column,
22
+ to: :configuration
23
+
24
+ def initialize(properties, measure:)
25
+ self.configuration = InferrableConfiguration.new(self, :filters)
26
+ self.measure = measure
27
+
28
+ properties = { key: properties } if properties.is_a?(String)
29
+ raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
30
+ self.properties = properties.deep_symbolize_keys
31
+ self.properties[:criteria] = filter_type.default_criteria unless self.properties[:criteria]
32
+ self.properties = self.properties
33
+ end
34
+
35
+ def key
36
+ properties[:key]
37
+ end
38
+
39
+ def label
40
+ key.titleize
41
+ end
42
+
43
+ def settings
44
+ inferred_settings.merge(settings_from_model)
45
+ end
46
+
47
+ def inferred_settings
48
+ configuration.inferred_settings.merge(inferred_filter_settings)
49
+ end
50
+
51
+ def inferred_filter_settings
52
+ {
53
+ column: column
54
+ }
55
+ end
56
+
57
+ def type_klass
58
+ type_klass_for_configuration_strategy = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[configuration_strategy]
59
+ return type_klass_for_configuration_strategy if type_klass_for_configuration_strategy
60
+ type_klass_for_column_type = COLUMN_TYPES_FILTER_TYPE_CLASSES[column_type]
61
+ return type_klass_for_column_type if type_klass_for_column_type
62
+ return filter_type_class_from_model if configured_by_model?
63
+ raise ArgumentError.new("No configuration found for filter with key: '#{key}'")
64
+ end
65
+
66
+ def filter_type
67
+ type_klass.new(settings, properties)
68
+ end
69
+
70
+ def filter_type_class_from_model
71
+ return unless settings
72
+ type_key = settings[:type_key]
73
+ raise ArgumentError.new("No type specified for filter with key: '#{key}'") unless type_key
74
+ type_class = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[type_key]
75
+ raise ArgumentError.new("Invalid type ('#{type_key}') specified for filter with key: '#{key}'") unless type_class
76
+ type_class
77
+ end
78
+
79
+ def apply(relation)
80
+ filter_type.apply_filter(relation)
81
+ end
82
+ end
83
+ end
84
+ end