reports_kit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +83 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +468 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kit/application.js +14 -0
- data/app/assets/javascripts/reports_kit/lib/_init.js +8 -0
- data/app/assets/javascripts/reports_kit/lib/chart.js +39 -0
- data/app/assets/javascripts/reports_kit/lib/report.js +119 -0
- data/app/assets/javascripts/reports_kit/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kit/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kit/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kit/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kit/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kit/reports.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2.css +484 -0
- data/config/routes.rb +10 -0
- data/docs/images/chart_options.png +0 -0
- data/docs/images/dashed_line.png +0 -0
- data/docs/images/flights_by_carrier.png +0 -0
- data/docs/images/flights_by_carrier_and_flight_at.png +0 -0
- data/docs/images/flights_by_delay.png +0 -0
- data/docs/images/flights_by_flight_at.png +0 -0
- data/docs/images/flights_by_hours_delayed.png +0 -0
- data/docs/images/flights_with_check_box.png +0 -0
- data/docs/images/flights_with_configured_boolean.png +0 -0
- data/docs/images/flights_with_configured_datetime.png +0 -0
- data/docs/images/flights_with_configured_number.png +0 -0
- data/docs/images/flights_with_configured_string.png +0 -0
- data/docs/images/flights_with_date_range.png +0 -0
- data/docs/images/flights_with_filters.png +0 -0
- data/docs/images/flights_with_multi_autocomplete.png +0 -0
- data/docs/images/flights_with_string_filter.png +0 -0
- data/docs/images/horizontal_bar.png +0 -0
- data/docs/images/legend_right.png +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gists/doc.txt +58 -0
- data/lib/reports_kit.rb +17 -0
- data/lib/reports_kit/base_controller.rb +11 -0
- data/lib/reports_kit/configuration.rb +10 -0
- data/lib/reports_kit/engine.rb +21 -0
- data/lib/reports_kit/helper.rb +19 -0
- data/lib/reports_kit/model.rb +16 -0
- data/lib/reports_kit/model_configuration.rb +23 -0
- data/lib/reports_kit/rails.rb +5 -0
- data/lib/reports_kit/report_builder.rb +76 -0
- data/lib/reports_kit/reports/data/chart_options.rb +132 -0
- data/lib/reports_kit/reports/data/generate.rb +65 -0
- data/lib/reports_kit/reports/data/one_dimension.rb +71 -0
- data/lib/reports_kit/reports/data/two_dimensions.rb +129 -0
- data/lib/reports_kit/reports/data/utils.rb +79 -0
- data/lib/reports_kit/reports/dimension.rb +78 -0
- data/lib/reports_kit/reports/filter.rb +84 -0
- data/lib/reports_kit/reports/filter_types/base.rb +47 -0
- data/lib/reports_kit/reports/filter_types/boolean.rb +30 -0
- data/lib/reports_kit/reports/filter_types/datetime.rb +27 -0
- data/lib/reports_kit/reports/filter_types/number.rb +28 -0
- data/lib/reports_kit/reports/filter_types/records.rb +26 -0
- data/lib/reports_kit/reports/filter_types/string.rb +38 -0
- data/lib/reports_kit/reports/generate_autocomplete_results.rb +55 -0
- data/lib/reports_kit/reports/inferrable_configuration.rb +113 -0
- data/lib/reports_kit/reports/measure.rb +58 -0
- data/lib/reports_kit/reports_controller.rb +15 -0
- data/lib/reports_kit/resources_controller.rb +8 -0
- data/lib/reports_kit/version.rb +3 -0
- data/reports_kit.gemspec +23 -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/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +35 -0
- data/spec/fixtures/generate_outputs.yml +208 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +275 -0
- data/spec/reports_kit/reports/dimension_spec.rb +38 -0
- data/spec/reports_kit/reports/filter_spec.rb +38 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +13 -0
- data/spec/support/models/issue.rb +6 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/repo.rb +7 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +38 -0
- metadata +232 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Base
|
5
|
+
attr_accessor :settings, :properties
|
6
|
+
|
7
|
+
def initialize(settings, properties)
|
8
|
+
self.settings = settings || {}
|
9
|
+
self.properties = properties
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply_filter(records)
|
13
|
+
return records unless valid?
|
14
|
+
records = records.joins(joins) if joins.present?
|
15
|
+
return records if value.blank? && !is_a?(FilterTypes::Boolean)
|
16
|
+
apply_conditions(records)
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_criteria
|
20
|
+
self.class::DEFAULT_CRITERIA
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def apply_conditions(_records)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def criteria
|
30
|
+
properties[:criteria]
|
31
|
+
end
|
32
|
+
|
33
|
+
def value
|
34
|
+
criteria[:value]
|
35
|
+
end
|
36
|
+
|
37
|
+
def joins
|
38
|
+
settings[:joins]
|
39
|
+
end
|
40
|
+
|
41
|
+
def column
|
42
|
+
settings[:column] || properties[:key]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Boolean < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
operator: nil
|
7
|
+
}
|
8
|
+
|
9
|
+
def apply_conditions(records)
|
10
|
+
case criteria[:operator]
|
11
|
+
when true, 'true'
|
12
|
+
records.where("(#{column}) = true")
|
13
|
+
when false, 'false'
|
14
|
+
records.where("(#{column}) != true")
|
15
|
+
else
|
16
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
criteria[:operator].present?
|
22
|
+
end
|
23
|
+
|
24
|
+
def column
|
25
|
+
settings[:conditions] || properties[:key]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Datetime < Base
|
5
|
+
DEFAULT_CRITERIA = {
|
6
|
+
operator: 'between'
|
7
|
+
}
|
8
|
+
|
9
|
+
def apply_conditions(records)
|
10
|
+
case criteria[:operator]
|
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
|
+
records.where("#{column} IS NOT NULL").where("#{column} BETWEEN ? AND ?", start_at, end_at)
|
16
|
+
else
|
17
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
value.present?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
module FilterTypes
|
4
|
+
class Number < Base
|
5
|
+
def apply_conditions(records)
|
6
|
+
case criteria[:operator]
|
7
|
+
when '>'
|
8
|
+
records.where("#{column} > #{value.to_i}")
|
9
|
+
when '>='
|
10
|
+
records.where("#{column} >= #{value.to_i}")
|
11
|
+
when '<'
|
12
|
+
records.where("#{column} < #{value.to_i}")
|
13
|
+
when '<='
|
14
|
+
records.where("#{column} <= #{value.to_i}")
|
15
|
+
when '='
|
16
|
+
records.where("#{column} = #{value.to_i}")
|
17
|
+
else
|
18
|
+
raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
value.present?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ReportsKit
|
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 ReportsKit
|
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,55 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
class GenerateAutocompleteResults
|
4
|
+
attr_accessor :params, :measure_key, :filter_key, :context_record
|
5
|
+
|
6
|
+
def initialize(params, context_record: nil)
|
7
|
+
self.params = params
|
8
|
+
self.measure_key = params[:measure_key]
|
9
|
+
self.filter_key = params[:filter_key]
|
10
|
+
self.context_record = context_record
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
raise ArgumentError.new("Could not find a model for filter_key: '#{filter_key}'") unless model
|
15
|
+
results = model
|
16
|
+
results = results.public_send(scope) if scope
|
17
|
+
results = results.limit(10_000)
|
18
|
+
results = results.map { |result| { id: result.id, text: result.to_s } }
|
19
|
+
results = results.sort_by { |result| result[:text].downcase }
|
20
|
+
results = filter_results(results)
|
21
|
+
results
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def filter_results(results)
|
27
|
+
query = params[:q].try(:downcase)
|
28
|
+
if query.present?
|
29
|
+
results = results.to_a.select { |r| r[:text].downcase.include?(query) }
|
30
|
+
end
|
31
|
+
results
|
32
|
+
end
|
33
|
+
|
34
|
+
def model
|
35
|
+
@model ||= begin
|
36
|
+
measure = Measure.new(measure_key, context_record: context_record)
|
37
|
+
filter = Filter.new(filter_key, measure: measure)
|
38
|
+
filter.instance_class
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def scope
|
43
|
+
@scope ||= begin
|
44
|
+
scope = params[:scope]
|
45
|
+
return unless scope.present?
|
46
|
+
return unless model.try(:reports_kit_configuration) && model.reports_kit_configuration.autocomplete_scopes.present?
|
47
|
+
unless model.reports_kit_configuration.autocomplete_scopes.include?(scope)
|
48
|
+
raise ArgumentError.new("Unallowed scope '#{scope}' for model #{model.name}")
|
49
|
+
end
|
50
|
+
scope
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
class InferrableConfiguration
|
4
|
+
SUPPORTED_COLUMN_TYPES = [
|
5
|
+
:boolean,
|
6
|
+
:datetime,
|
7
|
+
:integer,
|
8
|
+
:string
|
9
|
+
]
|
10
|
+
|
11
|
+
attr_accessor :inferrable, :inferrable_type
|
12
|
+
|
13
|
+
delegate :key, :measure, to: :inferrable
|
14
|
+
|
15
|
+
def initialize(inferrable, inferrable_type)
|
16
|
+
self.inferrable = inferrable
|
17
|
+
self.inferrable_type = inferrable_type
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration_strategy
|
21
|
+
if settings_from_model.present?
|
22
|
+
:model
|
23
|
+
elsif reflection
|
24
|
+
:association
|
25
|
+
elsif column_type
|
26
|
+
:column
|
27
|
+
else
|
28
|
+
inferrable_type_string = inferrable_type.to_s.singularize
|
29
|
+
raise ArgumentError.new("No configuration found on the #{model_class} model for #{inferrable_type_string} with key: '#{key}'")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def configured_by_association?
|
34
|
+
configuration_strategy == :association
|
35
|
+
end
|
36
|
+
|
37
|
+
def configured_by_column?
|
38
|
+
configuration_strategy == :column
|
39
|
+
end
|
40
|
+
|
41
|
+
def configured_by_model?
|
42
|
+
configuration_strategy == :model
|
43
|
+
end
|
44
|
+
|
45
|
+
def configured_by_time?
|
46
|
+
column_type == :datetime
|
47
|
+
end
|
48
|
+
|
49
|
+
def settings_from_model
|
50
|
+
return {} if model_configuration.blank?
|
51
|
+
return {} if model_configuration.public_send(inferrable_type).blank?
|
52
|
+
config_hash = model_configuration.public_send(inferrable_type).find do |hash|
|
53
|
+
hash[:key] == key
|
54
|
+
end
|
55
|
+
config_hash || {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def reflection
|
59
|
+
model_class.reflect_on_association(key.to_sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
def instance_class
|
63
|
+
return reflection.class_name.constantize if reflection
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def column
|
68
|
+
return unless inferred_settings
|
69
|
+
inferred_settings[:column]
|
70
|
+
end
|
71
|
+
|
72
|
+
def inferred_settings
|
73
|
+
return { column: "#{model_class.table_name}.#{key}" } if configured_by_column?
|
74
|
+
if configured_by_association?
|
75
|
+
return { column: "#{model_class.table_name}.#{reflection.foreign_key}" } if reflection.macro == :belongs_to
|
76
|
+
return inferred_settings_from_has_many if inferred_settings_from_has_many
|
77
|
+
end
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
|
81
|
+
def inferred_settings_from_has_many
|
82
|
+
return unless reflection.macro == :has_many
|
83
|
+
through_reflection = reflection.through_reflection
|
84
|
+
if through_reflection
|
85
|
+
{
|
86
|
+
joins: through_reflection.name,
|
87
|
+
column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
|
88
|
+
}
|
89
|
+
else
|
90
|
+
{
|
91
|
+
joins: reflection.name,
|
92
|
+
column: "#{reflection.klass.table_name}.#{reflection.klass.primary_key}"
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def column_type
|
98
|
+
column_type = model_class.columns_hash[key.to_s].try(:type)
|
99
|
+
return column_type if SUPPORTED_COLUMN_TYPES.include?(column_type)
|
100
|
+
end
|
101
|
+
|
102
|
+
def model_configuration
|
103
|
+
return unless model_class && model_class.respond_to?(:reports_kit_configuration)
|
104
|
+
model_class.reports_kit_configuration
|
105
|
+
end
|
106
|
+
|
107
|
+
def model_class
|
108
|
+
return unless measure
|
109
|
+
measure.model_class
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
module Reports
|
3
|
+
class Measure
|
4
|
+
attr_accessor :properties, :filters, :context_record
|
5
|
+
|
6
|
+
def initialize(properties, context_record: nil)
|
7
|
+
properties = { key: properties } if properties.is_a?(String)
|
8
|
+
raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
|
9
|
+
properties = properties.deep_symbolize_keys
|
10
|
+
filter_hashes = properties.delete(:filters) || []
|
11
|
+
filter_hashes = filter_hashes.values if filter_hashes.is_a?(Hash) && filter_hashes.key?(:'0')
|
12
|
+
|
13
|
+
self.properties = properties
|
14
|
+
self.filters = filter_hashes.map { |filter_hash| Filter.new(filter_hash, measure: self) }
|
15
|
+
self.context_record = context_record
|
16
|
+
end
|
17
|
+
|
18
|
+
def key
|
19
|
+
properties[:key]
|
20
|
+
end
|
21
|
+
|
22
|
+
def label
|
23
|
+
key.pluralize.titleize
|
24
|
+
end
|
25
|
+
|
26
|
+
def aggregate_function
|
27
|
+
:count
|
28
|
+
end
|
29
|
+
|
30
|
+
def conditions
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def base_relation
|
35
|
+
return context_record.public_send(key.pluralize) if context_record
|
36
|
+
model_class
|
37
|
+
end
|
38
|
+
|
39
|
+
def model_class
|
40
|
+
key.camelize.constantize
|
41
|
+
end
|
42
|
+
|
43
|
+
def filtered_relation
|
44
|
+
relation = base_relation
|
45
|
+
filters.each do |filter|
|
46
|
+
relation = filter.apply(relation)
|
47
|
+
end
|
48
|
+
relation
|
49
|
+
end
|
50
|
+
|
51
|
+
def properties_with_filters
|
52
|
+
all_properties = properties
|
53
|
+
all_properties[:filters] = filters.map(&:properties)
|
54
|
+
all_properties
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ReportsKit
|
2
|
+
class ReportsController < ReportsKit::BaseController
|
3
|
+
def index
|
4
|
+
# RubyProf.start
|
5
|
+
properties = ActiveSupport::JSON.decode(params[:properties])
|
6
|
+
report_data = Reports::Data::Generate.new(properties, context_record: context_record).perform
|
7
|
+
# result = RubyProf.stop
|
8
|
+
# printer = RubyProf::CallStackPrinter.new(result)
|
9
|
+
# File.open(Rails.root.join('tmp', 'out.html'), 'w') do |file|
|
10
|
+
# printer.print(file, :min_percent => 2)
|
11
|
+
# end
|
12
|
+
render json: { data: report_data }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|