reports_kits 0.7.5 → 0.7.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +21 -0
- data/Appraisals +27 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +35 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kits/application.js +14 -0
- data/app/assets/javascripts/reports_kits/lib/_init.js +9 -0
- data/app/assets/javascripts/reports_kits/lib/chart.js +73 -0
- data/app/assets/javascripts/reports_kits/lib/report.js +135 -0
- data/app/assets/javascripts/reports_kits/lib/table.js +87 -0
- data/app/assets/javascripts/reports_kits/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kits/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kits/vendor/jquery.tablesorter.min.js +4 -0
- data/app/assets/javascripts/reports_kits/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kits/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kits/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kits/reports.css.sass +33 -0
- data/app/assets/stylesheets/reports_kits/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kits/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2.css +484 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +10 -0
- data/docs/images/demo.gif +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gemfiles/mysql.gemfile +7 -0
- data/gemfiles/mysql.gemfile.lock +167 -0
- data/gemfiles/postgresql.gemfile +7 -0
- data/gemfiles/postgresql.gemfile.lock +165 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile +8 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile.lock +168 -0
- data/gemfiles/rails_4_mysql.gemfile +8 -0
- data/gemfiles/rails_4_mysql.gemfile.lock +165 -0
- data/gemfiles/rails_4_postgresql.gemfile +8 -0
- data/gemfiles/rails_4_postgresql.gemfile.lock +163 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile +8 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile.lock +169 -0
- data/gemfiles/rails_5_mysql.gemfile +8 -0
- data/gemfiles/rails_5_mysql.gemfile.lock +171 -0
- data/gemfiles/rails_5_postgresql.gemfile +8 -0
- data/gemfiles/rails_5_postgresql.gemfile.lock +169 -0
- data/lib/reports_kits/base_controller.rb +17 -0
- data/lib/reports_kits/cache.rb +37 -0
- data/lib/reports_kits/configuration.rb +50 -0
- data/lib/reports_kits/engine.rb +21 -0
- data/lib/reports_kits/entity.rb +3 -0
- data/lib/reports_kits/filters_controller.rb +11 -0
- data/lib/reports_kits/form_builder.rb +66 -0
- data/lib/reports_kits/helper.rb +24 -0
- data/lib/reports_kits/model.rb +16 -0
- data/lib/reports_kits/model_configuration.rb +28 -0
- data/lib/reports_kits/normalized_params.rb +16 -0
- data/lib/reports_kits/order.rb +33 -0
- data/lib/reports_kits/relative_time.rb +42 -0
- data/lib/reports_kits/report_builder.rb +88 -0
- data/lib/reports_kits/reports/abstract_series.rb +9 -0
- data/lib/reports_kits/reports/adapters/mysql.rb +26 -0
- data/lib/reports_kits/reports/adapters/postgresql.rb +26 -0
- data/lib/reports_kits/reports/composite_series.rb +48 -0
- data/lib/reports_kits/reports/contextual_filter.rb +19 -0
- data/lib/reports_kits/reports/data/add_table_aggregations.rb +105 -0
- data/lib/reports_kits/reports/data/aggregate_composite.rb +97 -0
- data/lib/reports_kits/reports/data/aggregate_one_dimension.rb +39 -0
- data/lib/reports_kits/reports/data/aggregate_two_dimensions.rb +39 -0
- data/lib/reports_kits/reports/data/chart_data_for_data_method.rb +62 -0
- data/lib/reports_kits/reports/data/chart_options.rb +155 -0
- data/lib/reports_kits/reports/data/format_one_dimension.rb +140 -0
- data/lib/reports_kits/reports/data/format_table.rb +63 -0
- data/lib/reports_kits/reports/data/format_two_dimensions.rb +143 -0
- data/lib/reports_kits/reports/data/generate.rb +156 -0
- data/lib/reports_kits/reports/data/generate_for_properties.rb +97 -0
- data/lib/reports_kits/reports/data/normalize_properties.rb +62 -0
- data/lib/reports_kits/reports/data/populate_one_dimension.rb +54 -0
- data/lib/reports_kits/reports/data/populate_two_dimensions.rb +104 -0
- data/lib/reports_kits/reports/data/utils.rb +178 -0
- data/lib/reports_kits/reports/dimension.rb +27 -0
- data/lib/reports_kits/reports/dimension_with_series.rb +144 -0
- data/lib/reports_kits/reports/filter.rb +29 -0
- data/lib/reports_kits/reports/filter_types/base.rb +48 -0
- data/lib/reports_kits/reports/filter_types/boolean.rb +47 -0
- data/lib/reports_kits/reports/filter_types/datetime.rb +51 -0
- data/lib/reports_kits/reports/filter_types/number.rb +30 -0
- data/lib/reports_kits/reports/filter_types/records.rb +26 -0
- data/lib/reports_kits/reports/filter_types/string.rb +38 -0
- data/lib/reports_kits/reports/filter_with_series.rb +97 -0
- data/lib/reports_kits/reports/generate_autocomplete_method_results.rb +29 -0
- data/lib/reports_kits/reports/generate_autocomplete_results.rb +57 -0
- data/lib/reports_kits/reports/inferrable_configuration.rb +116 -0
- data/lib/reports_kits/reports/model_settings.rb +30 -0
- data/lib/reports_kits/reports/properties.rb +10 -0
- data/lib/reports_kits/reports/properties_to_filter.rb +40 -0
- data/lib/reports_kits/reports/series.rb +121 -0
- data/lib/reports_kits/reports_controller.rb +65 -0
- data/lib/reports_kits/utils.rb +11 -0
- data/lib/reports_kits/value.rb +3 -0
- data/lib/reports_kits/version.rb +3 -0
- data/lib/reports_kits.rb +79 -0
- data/reports_kits.gemspec +26 -0
- data/spec/factories/issue_factory.rb +4 -0
- data/spec/factories/issues_label_factory.rb +4 -0
- data/spec/factories/label_factory.rb +4 -0
- data/spec/factories/pro_repo_factory.rb +5 -0
- data/spec/factories/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +254 -0
- data/spec/fixtures/generate_outputs.yml +1216 -0
- data/spec/reports_kit/form_builder_spec.rb +26 -0
- data/spec/reports_kit/relative_time_spec.rb +29 -0
- data/spec/reports_kit/reports/data/generate/contextual_filters_spec.rb +60 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +1371 -0
- data/spec/reports_kit/reports/data/normalize_properties_spec.rb +196 -0
- data/spec/reports_kit/reports/dimension_with_series_spec.rb +67 -0
- data/spec/reports_kit/reports/filter_with_series_spec.rb +39 -0
- data/spec/reports_kit/reports/generate_autocomplete_results_spec.rb +69 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/support/config.rb +41 -0
- data/spec/support/example_data_methods.rb +25 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +25 -0
- data/spec/support/models/issue.rb +14 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/pro/repo.rb +5 -0
- data/spec/support/models/pro/special_issue.rb +4 -0
- data/spec/support/models/repo.rb +13 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +39 -0
- metadata +134 -4
@@ -0,0 +1,144 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
class DimensionWithSeries
|
4
|
+
DEFAULT_GRANULARITY = 'week'
|
5
|
+
VALID_GRANULARITIES = %w(day week month).freeze
|
6
|
+
ADAPTER_NAMES_CLASSES = {
|
7
|
+
'mysql2' => Adapters::Mysql,
|
8
|
+
'postgresql' => Adapters::Postgresql
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_accessor :dimension, :series, :configuration
|
12
|
+
|
13
|
+
delegate :key, :expression, :properties, :label, to: :dimension
|
14
|
+
delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
|
15
|
+
:settings_from_model, :reflection, :instance_class, :model_class, :column_type,
|
16
|
+
to: :configuration
|
17
|
+
|
18
|
+
def initialize(dimension:, series:)
|
19
|
+
self.dimension = dimension
|
20
|
+
self.series = series
|
21
|
+
self.configuration = InferrableConfiguration.new(self, :dimensions)
|
22
|
+
missing_group_setting = settings && !settings.key?(:group)
|
23
|
+
raise ArgumentError.new("Dimension settings for dimension '#{key}' of #{model_class} must include :group") if missing_group_setting
|
24
|
+
end
|
25
|
+
|
26
|
+
def granularity
|
27
|
+
@granularity ||= begin
|
28
|
+
return unless configured_by_time?
|
29
|
+
granularity = properties[:granularity] || DEFAULT_GRANULARITY
|
30
|
+
raise ArgumentError.new("Invalid granularity: #{granularity}") unless VALID_GRANULARITIES.include?(granularity)
|
31
|
+
granularity
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def settings
|
36
|
+
inferred_settings.merge(settings_from_model)
|
37
|
+
end
|
38
|
+
|
39
|
+
def inferred_settings
|
40
|
+
configuration.inferred_settings.merge(inferred_dimension_settings)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inferred_dimension_settings
|
44
|
+
{
|
45
|
+
group: group_expression
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def group_expression
|
50
|
+
if configured_by_model?
|
51
|
+
settings_from_model[:group]
|
52
|
+
elsif configured_by_association?
|
53
|
+
inferred_settings_from_association[:column]
|
54
|
+
elsif configured_by_column? && configured_by_time?
|
55
|
+
case granularity
|
56
|
+
when 'day' then day_expression
|
57
|
+
when 'month' then month_expression
|
58
|
+
else week_expression
|
59
|
+
end
|
60
|
+
elsif configured_by_column?
|
61
|
+
column_expression
|
62
|
+
else
|
63
|
+
raise ArgumentError.new('Invalid group_expression')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def inferred_settings_from_association
|
68
|
+
through_reflection = reflection.through_reflection
|
69
|
+
if through_reflection
|
70
|
+
{
|
71
|
+
joins: through_reflection.name,
|
72
|
+
column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
|
73
|
+
}
|
74
|
+
else
|
75
|
+
{
|
76
|
+
column: "#{model_class.table_name}.#{reflection.foreign_key}"
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def joins
|
82
|
+
return settings_from_model[:joins] if configured_by_model?
|
83
|
+
inferred_settings_from_association[:joins] if configured_by_association?
|
84
|
+
end
|
85
|
+
|
86
|
+
def dimension_instances_limit
|
87
|
+
if configured_by_time?
|
88
|
+
properties[:limit]
|
89
|
+
else
|
90
|
+
properties[:limit] || ReportsKits.configuration.default_dimension_limit
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def first_key
|
95
|
+
return unless configured_by_time? && datetime_filters.present?
|
96
|
+
datetime_filters.map(&:start_at).compact.sort.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def last_key
|
100
|
+
return unless configured_by_time? && datetime_filters.present?
|
101
|
+
datetime_filters.map(&:end_at).compact.sort.last
|
102
|
+
end
|
103
|
+
|
104
|
+
def key_to_label(key)
|
105
|
+
return unless settings[:key_to_label]
|
106
|
+
settings[:key_to_label].call(key)
|
107
|
+
end
|
108
|
+
|
109
|
+
def datetime_filters
|
110
|
+
return [] unless series.filters.present?
|
111
|
+
series.filters.map(&:filter_type).select { |filter_type| filter_type.is_a?(FilterTypes::Datetime) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def should_be_sorted_by_count?
|
115
|
+
!configured_by_time?
|
116
|
+
end
|
117
|
+
|
118
|
+
def adapter
|
119
|
+
@adapter ||= begin
|
120
|
+
adapter_name = model_class.connection_config[:adapter]
|
121
|
+
adapter = ADAPTER_NAMES_CLASSES[adapter_name]
|
122
|
+
raise ArgumentError.new("Unsupported adapter: #{adapter_name}") unless adapter
|
123
|
+
adapter
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def column_expression
|
128
|
+
"#{model_class.table_name}.#{key}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def day_expression
|
132
|
+
adapter.truncate_to_day(column_expression)
|
133
|
+
end
|
134
|
+
|
135
|
+
def week_expression
|
136
|
+
adapter.truncate_to_week(column_expression)
|
137
|
+
end
|
138
|
+
|
139
|
+
def month_expression
|
140
|
+
adapter.truncate_to_month(column_expression)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
class Filter
|
4
|
+
attr_accessor :properties
|
5
|
+
|
6
|
+
def initialize(properties)
|
7
|
+
properties = { key: properties } if properties.is_a?(String)
|
8
|
+
raise ArgumentError.new("Filter properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
|
9
|
+
self.properties = properties.deep_symbolize_keys
|
10
|
+
end
|
11
|
+
|
12
|
+
def key
|
13
|
+
properties[:key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def expression
|
17
|
+
properties[:expression] || key
|
18
|
+
end
|
19
|
+
|
20
|
+
def label
|
21
|
+
key.titleize
|
22
|
+
end
|
23
|
+
|
24
|
+
def normalized_properties
|
25
|
+
properties
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Base
|
5
|
+
attr_accessor :settings, :properties, :primary_dimension
|
6
|
+
|
7
|
+
def initialize(settings, properties, primary_dimension:)
|
8
|
+
self.settings = settings || {}
|
9
|
+
self.properties = properties
|
10
|
+
self.primary_dimension = primary_dimension
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply_filter(records)
|
14
|
+
return records unless valid?
|
15
|
+
records = records.joins(joins) if joins.present?
|
16
|
+
return records if value.blank? && !is_a?(FilterTypes::Boolean)
|
17
|
+
apply_conditions(records)
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_criteria
|
21
|
+
self.class::DEFAULT_CRITERIA
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def apply_conditions(_records)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def criteria
|
31
|
+
@criteria ||= default_criteria.merge(properties[:criteria])
|
32
|
+
end
|
33
|
+
|
34
|
+
def value
|
35
|
+
criteria[:value]
|
36
|
+
end
|
37
|
+
|
38
|
+
def joins
|
39
|
+
settings[:joins]
|
40
|
+
end
|
41
|
+
|
42
|
+
def column
|
43
|
+
settings[:column] || Data::Utils.quote_column_name(properties[:key])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Boolean < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
value: nil
|
7
|
+
}
|
8
|
+
|
9
|
+
def apply_conditions(records)
|
10
|
+
case conditions
|
11
|
+
when ::String
|
12
|
+
records.where("(#{conditions}) #{sql_operator} true")
|
13
|
+
when ::Hash
|
14
|
+
boolean_value ? records.where(conditions) : records.not.where(conditions)
|
15
|
+
when ::Proc
|
16
|
+
conditions.call(records)
|
17
|
+
else
|
18
|
+
raise ArgumentError.new("Unsupported conditions type: '#{conditions}'")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def boolean_value
|
23
|
+
case value
|
24
|
+
when true, 'true'
|
25
|
+
true
|
26
|
+
when false, 'false'
|
27
|
+
false
|
28
|
+
else
|
29
|
+
raise ArgumentError.new("Unsupported value: '#{value}'")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def sql_operator
|
34
|
+
boolean_value ? '=' : '!='
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid?
|
38
|
+
value.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def conditions
|
42
|
+
settings[:conditions] || Data::Utils.quote_column_name(properties[:key])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Datetime < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
operator: 'between'
|
7
|
+
}
|
8
|
+
SEPARATOR = ' - '
|
9
|
+
|
10
|
+
def apply_conditions(records)
|
11
|
+
case criteria[:operator]
|
12
|
+
when 'between'
|
13
|
+
records.where.not(column => nil).where(column => start_at..end_at)
|
14
|
+
else
|
15
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def start_at_end_at
|
20
|
+
@start_at_end_at ||= valid? ? adjust_range_to_dimension(*Reports::Data::Utils.parse_date_range(value, type: Array)) : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_at
|
24
|
+
start_at_end_at.try(:[], 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
def end_at
|
28
|
+
start_at_end_at.try(:[], 1)
|
29
|
+
end
|
30
|
+
|
31
|
+
def adjust_range_to_dimension(start_at, end_at)
|
32
|
+
return [start_at.beginning_of_day, end_at.end_of_day] unless primary_dimension.configured_by_time?
|
33
|
+
return [start_at.beginning_of_day, end_at.end_of_day] if primary_dimension.granularity == 'day'
|
34
|
+
return [
|
35
|
+
start_at.beginning_of_week(ReportsKits.configuration.first_day_of_week),
|
36
|
+
end_at.end_of_week(ReportsKits.configuration.first_day_of_week)
|
37
|
+
] if primary_dimension.granularity == 'week'
|
38
|
+
return [
|
39
|
+
start_at.beginning_of_month,
|
40
|
+
end_at.end_of_month
|
41
|
+
] if primary_dimension.granularity == 'month'
|
42
|
+
[start_at, end_at]
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid?
|
46
|
+
value.present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Number < Base
|
5
|
+
DEFAULT_CRITERIA = {}
|
6
|
+
|
7
|
+
def apply_conditions(records)
|
8
|
+
case criteria[:operator]
|
9
|
+
when '>'
|
10
|
+
records.where(column => (value.to_i...Float::INFINITY))
|
11
|
+
when '>='
|
12
|
+
records.where(column => (value.to_i..Float::INFINITY))
|
13
|
+
when '<'
|
14
|
+
records.where(column => (-Float::INFINITY...value.to_i))
|
15
|
+
when '<='
|
16
|
+
records.where(column => (-Float::INFINITY..value.to_i))
|
17
|
+
when '='
|
18
|
+
records.where(column => value.to_i)
|
19
|
+
else
|
20
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
value.present?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Records < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
operator: 'include'
|
7
|
+
}
|
8
|
+
|
9
|
+
def apply_conditions(records)
|
10
|
+
case criteria[:operator]
|
11
|
+
when 'include'
|
12
|
+
records.where(column => value)
|
13
|
+
when 'does_not_include'
|
14
|
+
records.where.not(column => value)
|
15
|
+
else
|
16
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
value.present?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class String < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
operator: 'contains'
|
7
|
+
}
|
8
|
+
|
9
|
+
def apply_conditions(records)
|
10
|
+
case criteria[:operator]
|
11
|
+
when 'equals'
|
12
|
+
records.where("#{column} = ?", value)
|
13
|
+
when 'contains'
|
14
|
+
records.where("#{column} ILIKE ?", "%#{value}%")
|
15
|
+
when 'starts_with'
|
16
|
+
records.where("#{column} ILIKE ?", "#{value}%")
|
17
|
+
when 'ends_with'
|
18
|
+
records.where("#{column} ILIKE ?", "%#{value}")
|
19
|
+
when 'does_not_equal'
|
20
|
+
records.where("#{column} != ?", value)
|
21
|
+
when 'does_not_contain'
|
22
|
+
records.where("#{column} NOT ILIKE ?", "%#{value}%")
|
23
|
+
when 'does_not_start_with'
|
24
|
+
records.where("#{column} NOT ILIKE ?", "#{value}%")
|
25
|
+
when 'does_not_end_with'
|
26
|
+
records.where("#{column} NOT ILIKE ?", "%#{value}")
|
27
|
+
else
|
28
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
value.present?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
class FilterWithSeries
|
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 :filter, :series, :configuration
|
19
|
+
|
20
|
+
delegate :key, :expression, :properties, :label, to: :filter
|
21
|
+
delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
|
22
|
+
:settings_from_model, :configuration_strategy, :instance_class, :column_type, :column,
|
23
|
+
to: :configuration
|
24
|
+
|
25
|
+
def initialize(filter:, series:)
|
26
|
+
self.filter = filter
|
27
|
+
self.series = series
|
28
|
+
self.configuration = InferrableConfiguration.new(self, :filters)
|
29
|
+
|
30
|
+
properties[:criteria] = filter_type.default_criteria unless properties[:criteria]
|
31
|
+
end
|
32
|
+
|
33
|
+
def normalized_properties
|
34
|
+
return properties unless configured_by_time?
|
35
|
+
criteria = properties[:criteria]
|
36
|
+
return properties if criteria.blank? || criteria[:value].blank?
|
37
|
+
values = criteria[:value].split(ReportsKits::Reports::FilterTypes::Datetime::SEPARATOR)
|
38
|
+
values = values.map { |value| ReportsKits::Reports::Data::Utils.format_time_value(value) }
|
39
|
+
properties[:criteria][:value] = values.join(ReportsKits::Reports::FilterTypes::Datetime::SEPARATOR)
|
40
|
+
properties
|
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 context_record_association
|
58
|
+
properties[:context_record_association] || instance_class.name.tableize
|
59
|
+
end
|
60
|
+
|
61
|
+
def type_klass
|
62
|
+
type_klass_for_configuration_strategy = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[configuration_strategy]
|
63
|
+
return type_klass_for_configuration_strategy if type_klass_for_configuration_strategy
|
64
|
+
type_klass_for_column_type = COLUMN_TYPES_FILTER_TYPE_CLASSES[column_type]
|
65
|
+
return type_klass_for_column_type if type_klass_for_column_type
|
66
|
+
return filter_type_class_from_model if configured_by_model?
|
67
|
+
raise ArgumentError.new("No configuration found for filter with key: '#{key}'")
|
68
|
+
end
|
69
|
+
|
70
|
+
def filter_type
|
71
|
+
type_klass.new(settings, properties, primary_dimension: series.dimensions.first)
|
72
|
+
end
|
73
|
+
|
74
|
+
def filter_type_class_from_model
|
75
|
+
return unless settings
|
76
|
+
type_key = settings[:type_key]
|
77
|
+
raise ArgumentError.new("No type specified for filter with key: '#{key}'") unless type_key
|
78
|
+
type_class = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[type_key]
|
79
|
+
raise ArgumentError.new("Invalid type ('#{type_key}') specified for filter with key: '#{key}'") unless type_class
|
80
|
+
type_class
|
81
|
+
end
|
82
|
+
|
83
|
+
def apply_contextual_filters(relation, context_params)
|
84
|
+
return relation if properties[:contextual_filters].blank? || context_params.blank?
|
85
|
+
contextual_filters = properties[:contextual_filters].map { |key| ContextualFilter.new(key, relation.base_class) }
|
86
|
+
contextual_filters.each do |contextual_filter|
|
87
|
+
relation = contextual_filter.apply(relation, context_params)
|
88
|
+
end
|
89
|
+
relation
|
90
|
+
end
|
91
|
+
|
92
|
+
def apply(relation)
|
93
|
+
filter_type.apply_filter(relation)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
class GenerateAutocompleteMethodResults
|
4
|
+
attr_accessor :filter_key, :params, :properties
|
5
|
+
|
6
|
+
def initialize(filter_key, properties, params)
|
7
|
+
self.filter_key = filter_key
|
8
|
+
self.params = params
|
9
|
+
self.properties = properties
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
return unless properties[:ui_filters]
|
14
|
+
klass, method_name = ReportsKits::Utils.string_to_class_method(autocomplete_method, 'autocomplete_method')
|
15
|
+
klass.public_send(method_name, params, properties)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def filter_hash
|
21
|
+
properties[:ui_filters].find { |filter_params| filter_params.is_a?(Hash) && filter_params[:key] == filter_key }
|
22
|
+
end
|
23
|
+
|
24
|
+
def autocomplete_method
|
25
|
+
filter_hash[:autocomplete_method]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ReportsKits
|
2
|
+
module Reports
|
3
|
+
class GenerateAutocompleteResults
|
4
|
+
attr_accessor :params, :properties, :filter_key, :filter, :context_params, :context_record
|
5
|
+
|
6
|
+
def initialize(params, properties, context_record: nil)
|
7
|
+
self.params = params
|
8
|
+
self.properties = properties
|
9
|
+
self.context_params = params[:context_params]
|
10
|
+
self.filter_key = params[:key]
|
11
|
+
self.filter = Reports::PropertiesToFilter.new(properties, context_record: context_record).perform(filter_key)
|
12
|
+
self.context_record = context_record
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
autocomplete_method_results = Reports::GenerateAutocompleteMethodResults.new(filter_key, properties, params).perform
|
17
|
+
return autocomplete_method_results if autocomplete_method_results
|
18
|
+
raise ArgumentError.new("Could not find a model for filter_key: '#{filter_key}'") unless model
|
19
|
+
return autocomplete_results_method.call(params: params, context_record: context_record, relation: relation) if autocomplete_results_method
|
20
|
+
results = filter.apply_contextual_filters(relation, context_params)
|
21
|
+
results = results.limit(10_000)
|
22
|
+
results = results.map { |result| { id: result.id, text: result.to_s } }
|
23
|
+
results = results.sort_by { |result| result[:text].downcase }
|
24
|
+
results = filter_results(results)
|
25
|
+
results.first(100)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def autocomplete_results_method
|
31
|
+
ReportsKits.configuration.autocomplete_results_method
|
32
|
+
end
|
33
|
+
|
34
|
+
def relation
|
35
|
+
if context_record
|
36
|
+
context_record.public_send(filter.context_record_association)
|
37
|
+
else
|
38
|
+
model
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def filter_results(results)
|
43
|
+
query = params[:q].try(:downcase)
|
44
|
+
if query.present?
|
45
|
+
results = results.to_a.select { |r| r[:text].downcase.include?(query) }
|
46
|
+
end
|
47
|
+
results
|
48
|
+
end
|
49
|
+
|
50
|
+
def model
|
51
|
+
@model ||= begin
|
52
|
+
filter.instance_class
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|